Merge "Revert "Functional spec for cloud-init support""
[osm/SO.git] / models / openmano / python / rift / openmano / openmano_client.py
1 #!/usr/bin/python3
2
3 #
4 # Copyright 2016 RIFT.IO Inc
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #
18
19 import argparse
20 import logging
21 import os
22 import re
23 import subprocess
24 import sys
25 import tempfile
26 import requests
27 import json
28
29
30 class OpenmanoCommandFailed(Exception):
31 pass
32
33
34 class OpenmanoUnexpectedOutput(Exception):
35 pass
36
37
38 class VNFExistsError(Exception):
39 pass
40
41
42 class InstanceStatusError(Exception):
43 pass
44
45
46 class OpenmanoHttpAPI(object):
47 def __init__(self, log, host, port, tenant):
48 self._log = log
49 self._host = host
50 self._port = port
51 self._tenant = tenant
52
53 self._session = requests.Session()
54
55 def get_instance(self, instance_uuid):
56 url = "http://{host}:{port}/openmano/{tenant}/instances/{instance}".format(
57 host=self._host,
58 port=self._port,
59 tenant=self._tenant,
60 instance=instance_uuid,
61 )
62
63 resp = self._session.get(url)
64 try:
65 resp.raise_for_status()
66 except requests.exceptions.HTTPError as e:
67 raise InstanceStatusError(e)
68
69 return resp.json()
70
71 def get_instance_vm_console_url(self, instance_uuid, vim_uuid):
72 url = "http://{host}:{port}/openmano/{tenant}/instances/{instance}/action".format(
73 host=self._host,
74 port=self._port,
75 tenant=self._tenant,
76 instance=instance_uuid,
77 )
78
79 console_types = ("novnc", "spice-html5", "xvpnvc", "rdp-html5")
80 for console_type in console_types:
81 payload_input = {"console":console_type, "vms":[vim_uuid]}
82 payload_data = json.dumps(payload_input)
83 resp = self._session.post(url, headers={'content-type': 'application/json'},
84 data=payload_data)
85 try:
86 resp.raise_for_status()
87 except requests.exceptions.HTTPError as e:
88 raise InstanceStatusError(e)
89 result = resp.json()
90 if vim_uuid in result and (result[vim_uuid]["vim_result"] == 1 or result[vim_uuid]["vim_result"] == 200):
91 return result[vim_uuid]["description"]
92
93 return None
94
95
96 class OpenmanoCliAPI(object):
97 """ This class implements the necessary funtionality to interact with """
98
99 CMD_TIMEOUT = 30
100
101 def __init__(self, log, host, port, tenant):
102 self._log = log
103 self._host = host
104 self._port = port
105 self._tenant = tenant
106
107 @staticmethod
108 def openmano_cmd_path():
109 return os.path.join(
110 os.environ["RIFT_INSTALL"],
111 "usr/bin/openmano"
112 )
113
114 def _openmano_cmd(self, arg_list, expected_lines=None):
115 cmd_args = list(arg_list)
116 cmd_args.insert(0, self.openmano_cmd_path())
117
118 env = {
119 "OPENMANO_HOST": self._host,
120 "OPENMANO_PORT": str(self._port),
121 "OPENMANO_TENANT": self._tenant,
122 }
123
124 self._log.debug(
125 "Running openmano command (%s) using env (%s)",
126 subprocess.list2cmdline(cmd_args),
127 env,
128 )
129
130 proc = subprocess.Popen(
131 cmd_args,
132 stdout=subprocess.PIPE,
133 stderr=subprocess.PIPE,
134 universal_newlines=True,
135 env=env
136 )
137 try:
138 stdout, stderr = proc.communicate(timeout=self.CMD_TIMEOUT)
139 except subprocess.TimeoutExpired:
140 self._log.error("Openmano command timed out")
141 proc.terminate()
142 stdout, stderr = proc.communicate(timeout=self.CMD_TIMEOUT)
143
144 if proc.returncode != 0:
145 self._log.error(
146 "Openmano command failed (rc=%s) with stdout: %s",
147 proc.returncode, stdout
148 )
149 raise OpenmanoCommandFailed(stdout)
150
151 self._log.debug("Openmano command completed with stdout: %s", stdout)
152
153 output_lines = stdout.splitlines()
154 if expected_lines is not None:
155 if len(output_lines) != expected_lines:
156 msg = "Expected %s lines from openmano command. Got %s" % (expected_lines, len(output_lines))
157 self._log.error(msg)
158 raise OpenmanoUnexpectedOutput(msg)
159
160 return output_lines
161
162
163 def vnf_create(self, vnf_yaml_str):
164 """ Create a Openmano VNF from a Openmano VNF YAML string """
165
166 self._log.debug("Creating VNF: %s", vnf_yaml_str)
167
168 with tempfile.NamedTemporaryFile() as vnf_file_hdl:
169 vnf_file_hdl.write(vnf_yaml_str.encode())
170 vnf_file_hdl.flush()
171
172 try:
173 output_lines = self._openmano_cmd(
174 ["vnf-create", vnf_file_hdl.name],
175 expected_lines=1
176 )
177 except OpenmanoCommandFailed as e:
178 if "already in use" in str(e):
179 raise VNFExistsError("VNF was already added")
180 raise
181
182 vnf_info_line = output_lines[0]
183 vnf_id, vnf_name = vnf_info_line.split(" ", 1)
184
185 self._log.info("VNF %s Created: %s", vnf_name, vnf_id)
186
187 return vnf_id, vnf_name
188
189 def vnf_delete(self, vnf_uuid):
190 self._openmano_cmd(
191 ["vnf-delete", vnf_uuid, "-f"],
192 )
193
194 self._log.info("VNF Deleted: %s", vnf_uuid)
195
196 def vnf_list(self):
197 try:
198 output_lines = self._openmano_cmd(
199 ["vnf-list"],
200 )
201 except OpenmanoCommandFailed as e:
202 self._log.warning("Vnf listing returned an error: %s", str(e))
203 return {}
204
205 name_uuid_map = {}
206 for line in output_lines:
207 line = line.strip()
208 uuid, name = line.split(" ", 1)
209 name_uuid_map[name] = uuid
210
211 return name_uuid_map
212
213 def ns_create(self, ns_yaml_str, name=None):
214 self._log.info("Creating NS: %s", ns_yaml_str)
215
216 with tempfile.NamedTemporaryFile() as ns_file_hdl:
217 ns_file_hdl.write(ns_yaml_str.encode())
218 ns_file_hdl.flush()
219
220 cmd_args = ["scenario-create", ns_file_hdl.name]
221 if name is not None:
222 cmd_args.extend(["--name", name])
223
224 output_lines = self._openmano_cmd(
225 cmd_args,
226 expected_lines=1
227 )
228
229 ns_info_line = output_lines[0]
230 ns_id, ns_name = ns_info_line.split(" ", 1)
231
232 self._log.info("NS %s Created: %s", ns_name, ns_id)
233
234 return ns_id, ns_name
235
236 def ns_list(self):
237 self._log.debug("Getting NS list")
238
239 try:
240 output_lines = self._openmano_cmd(
241 ["scenario-list"],
242 )
243
244 except OpenmanoCommandFailed as e:
245 self._log.warning("NS listing returned an error: %s", str(e))
246 return {}
247
248 name_uuid_map = {}
249 for line in output_lines:
250 line = line.strip()
251 uuid, name = line.split(" ", 1)
252 name_uuid_map[name] = uuid
253
254 return name_uuid_map
255
256 def ns_delete(self, ns_uuid):
257 self._log.info("Deleting NS: %s", ns_uuid)
258
259 self._openmano_cmd(
260 ["scenario-delete", ns_uuid, "-f"],
261 )
262
263 self._log.info("NS Deleted: %s", ns_uuid)
264
265 def ns_instance_list(self):
266 self._log.debug("Getting NS instance list")
267
268 try:
269 output_lines = self._openmano_cmd(
270 ["instance-scenario-list"],
271 )
272
273 except OpenmanoCommandFailed as e:
274 self._log.warning("Instance scenario listing returned an error: %s", str(e))
275 return {}
276
277 if "No scenario instances were found" in output_lines[0]:
278 self._log.debug("No openmano instances were found")
279 return {}
280
281 name_uuid_map = {}
282 for line in output_lines:
283 line = line.strip()
284 uuid, name = line.split(" ", 1)
285 name_uuid_map[name] = uuid
286
287 return name_uuid_map
288
289 def ns_instance_scenario_create(self, instance_yaml_str):
290 """ Create a Openmano NS instance from input YAML string """
291
292 self._log.debug("Instantiating instance: %s", instance_yaml_str)
293
294 with tempfile.NamedTemporaryFile() as ns_instance_file_hdl:
295 ns_instance_file_hdl.write(instance_yaml_str.encode())
296 ns_instance_file_hdl.flush()
297
298 try:
299 output_lines = self._openmano_cmd(
300 ["instance-scenario-create", ns_instance_file_hdl.name],
301 expected_lines=1
302 )
303 except OpenmanoCommandFailed as e:
304 raise
305
306 uuid, _ = output_lines[0].split(" ", 1)
307
308 self._log.info("NS Instance Created: %s", uuid)
309
310 return uuid
311
312 def ns_instantiate(self, scenario_name, instance_name, datacenter_name=None):
313 self._log.info(
314 "Instantiating NS %s using instance name %s",
315 scenario_name,
316 instance_name,
317 )
318
319 cmd_args = ["scenario-deploy", scenario_name, instance_name]
320 if datacenter_name is not None:
321 cmd_args.extend(["--datacenter", datacenter_name])
322
323 output_lines = self._openmano_cmd(
324 cmd_args,
325 expected_lines=4
326 )
327
328 uuid, _ = output_lines[0].split(" ", 1)
329
330 self._log.info("NS Instance Created: %s", uuid)
331
332 return uuid
333
334 def ns_terminate(self, ns_instance_name):
335 self._log.info("Terminating NS: %s", ns_instance_name)
336
337 self._openmano_cmd(
338 ["instance-scenario-delete", ns_instance_name, "-f"],
339 )
340
341 self._log.info("NS Instance Deleted: %s", ns_instance_name)
342
343 def datacenter_list(self):
344 lines = self._openmano_cmd(["datacenter-list",])
345
346 # The results returned from openmano are formatted with whitespace and
347 # datacenter names may contain whitespace as well, so we use a regular
348 # expression to parse each line of the results return from openmano to
349 # extract the uuid and name of a datacenter.
350 hex = '[0-9a-fA-F]'
351 uuid_pattern = '(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)'.replace('x', hex)
352 name_pattern = '(.+?)'
353 datacenter_regex = re.compile(r'{uuid}\s+\b{name}\s*$'.format(
354 uuid=uuid_pattern,
355 name=name_pattern,
356 ))
357
358 # Parse the results for the datacenter uuids and names
359 datacenters = list()
360 for line in lines:
361 result = datacenter_regex.match(line)
362 if result is not None:
363 uuid, name = result.groups()
364 datacenters.append((uuid, name))
365
366 return datacenters
367
368
369 def valid_uuid(uuid_str):
370 uuid_re = re.compile(
371 "^xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx$".replace('x', '[0-9a-fA-F]')
372 )
373
374 if not uuid_re.match(uuid_str):
375 raise argparse.ArgumentTypeError("Got a valid uuid: %s" % uuid_str)
376
377 return uuid_str
378
379
380 def parse_args(argv=sys.argv[1:]):
381 """ Parse the command line arguments
382
383 Arguments:
384 argv - The list of arguments to parse
385
386 Returns:
387 Argparse Namespace instance
388 """
389 parser = argparse.ArgumentParser()
390 parser.add_argument(
391 '-d', '--host',
392 default='localhost',
393 help="Openmano host/ip",
394 )
395
396 parser.add_argument(
397 '-p', '--port',
398 default='9090',
399 help="Openmano port",
400 )
401
402 parser.add_argument(
403 '-t', '--tenant',
404 required=True,
405 type=valid_uuid,
406 help="Openmano tenant uuid to use",
407 )
408
409 subparsers = parser.add_subparsers(dest='command', help='openmano commands')
410
411 vnf_create_parser = subparsers.add_parser(
412 'vnf-create',
413 help="Adds a openmano vnf into the catalog"
414 )
415 vnf_create_parser.add_argument(
416 "file",
417 help="location of the JSON file describing the VNF",
418 type=argparse.FileType('rb'),
419 )
420
421 vnf_delete_parser = subparsers.add_parser(
422 'vnf-delete',
423 help="Deletes a openmano vnf into the catalog"
424 )
425 vnf_delete_parser.add_argument(
426 "uuid",
427 help="The vnf to delete",
428 type=valid_uuid,
429 )
430
431
432 ns_create_parser = subparsers.add_parser(
433 'scenario-create',
434 help="Adds a openmano ns scenario into the catalog"
435 )
436 ns_create_parser.add_argument(
437 "file",
438 help="location of the JSON file describing the NS",
439 type=argparse.FileType('rb'),
440 )
441
442 ns_delete_parser = subparsers.add_parser(
443 'scenario-delete',
444 help="Deletes a openmano ns into the catalog"
445 )
446 ns_delete_parser.add_argument(
447 "uuid",
448 help="The ns to delete",
449 type=valid_uuid,
450 )
451
452
453 ns_instance_create_parser = subparsers.add_parser(
454 'scenario-deploy',
455 help="Deploys a openmano ns scenario into the catalog"
456 )
457 ns_instance_create_parser.add_argument(
458 "scenario_name",
459 help="The ns scenario name to deploy",
460 )
461 ns_instance_create_parser.add_argument(
462 "instance_name",
463 help="The ns instance name to deploy",
464 )
465
466
467 ns_instance_delete_parser = subparsers.add_parser(
468 'instance-scenario-delete',
469 help="Deploys a openmano ns scenario into the catalog"
470 )
471 ns_instance_delete_parser.add_argument(
472 "instance_name",
473 help="The ns instance name to delete",
474 )
475
476
477 _ = subparsers.add_parser(
478 'datacenter-list',
479 )
480
481 args = parser.parse_args(argv)
482
483 return args
484
485
486 def main():
487 logging.basicConfig(level=logging.DEBUG)
488 logger = logging.getLogger("openmano_client.py")
489
490 if "RIFT_INSTALL" not in os.environ:
491 logger.error("Must be in rift-shell to run.")
492 sys.exit(1)
493
494 args = parse_args()
495 openmano_cli = OpenmanoCliAPI(logger, args.host, args.port, args.tenant)
496
497 if args.command == "vnf-create":
498 openmano_cli.vnf_create(args.file.read())
499
500 elif args.command == "vnf-delete":
501 openmano_cli.vnf_delete(args.uuid)
502
503 elif args.command == "scenario-create":
504 openmano_cli.ns_create(args.file.read())
505
506 elif args.command == "scenario-delete":
507 openmano_cli.ns_delete(args.uuid)
508
509 elif args.command == "scenario-deploy":
510 openmano_cli.ns_instantiate(args.scenario_name, args.instance_name)
511
512 elif args.command == "instance-scenario-delete":
513 openmano_cli.ns_terminate(args.instance_name)
514
515 elif args.command == "datacenter-list":
516 for uuid, name in openmano_cli.datacenter_list():
517 print("{} {}".format(uuid, name))
518
519 else:
520 logger.error("Unknown command: %s", args.command)
521 sys.exit(1)
522
523 if __name__ == "__main__":
524 main()