Merge from OSM SO master
[osm/SO.git] / rwlaunchpad / plugins / rwlaunchpadtasklet / scripts / onboard_pkg
1 #!/usr/bin/env 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 from contextlib import closing
21 import logging
22 import os.path
23 import socket
24 import subprocess
25 import sys
26 import uuid
27
28 import json
29
30
31 class OnboardPkgError(Exception):
32     pass
33
34
35 class OnboardPkgInputError(OnboardPkgError):
36     pass
37
38
39 class OnboardPkgMissingPkg(OnboardPkgError):
40     pass
41
42
43 class OnboardPkgFileError(OnboardPkgError):
44     pass
45
46
47 class OnboardPkgMissingDescId(OnboardPkgError):
48     pass
49
50
51 class OnboardPkgInvalidDescId(OnboardPkgError):
52     pass
53
54
55 class OnboardPkgMissingAcct(OnboardPkgError):
56     pass
57
58
59 class OnboardPkgSoConnError(OnboardPkgError):
60     pass
61
62
63 class OnboardPkgCmdError(OnboardPkgError):
64     pass
65
66
67 class OnboardPkgUploadError(OnboardPkgError):
68     pass
69
70
71 class OnboardPkgRcConnError(OnboardPkgError):
72     pass
73
74
75 class OnboardPkgDcError(OnboardPkgError):
76     pass
77
78
79 class OnboardPkgAcctError(OnboardPkgError):
80     pass
81
82
83 class OnboardPkgNsdError(OnboardPkgError):
84     pass
85
86
87 class OnboardPkgInstError(OnboardPkgError):
88     pass
89
90
91 class OnboardPkgInvalidPort(OnboardPkgError):
92     pass
93
94
95 class OnboardPackage:
96
97     def __init__(self,
98                  log,
99                  args):
100         self._log = log
101         self._args = args
102
103         self._project = args.project
104
105         self._pkgs = None
106
107         self._service_name = None
108         self._nsd_id = None
109         self._dc = None
110         self._account = None
111
112         self._ip = args.so_ip
113         self._api_server_ip = "localhost"
114
115         self._uport = args.upload_port
116         self._onboard_port = args.onboard_port
117         self._rport = args.restconf_port
118         self._user = args.restconf_user
119         self._password = args.restconf_password
120         self._onboard_url = "curl -k --user \"{user}:{passwd}\" \"https://{ip}:{port}/composer/upload?api_server=https://{api_server_ip}&upload_server=https://{ip}\"". \
121                              format(ip=self._ip,
122                                     port=self._onboard_port,
123                                     user=self._user,
124                                     passwd=self._password,
125                                     api_server_ip=self._api_server_ip)
126
127         self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
128                             format(ip=self._ip,
129                                    port=self._uport)
130
131         self._headers = '-H "accept: application/json"' + \
132                         ' -H "content-type: application/json"'
133
134         self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config/project/{project}". \
135                        format(header=self._headers,
136                               user=self._user,
137                               passwd=self._password,
138                               ip=self._ip,
139                               port=self._rport,
140                               project=self._project)
141
142         self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational/project/{project}". \
143                        format(header=self._headers,
144                               user=self._user,
145                               passwd=self._password,
146                               ip=self._ip,
147                               port=self._rport,
148                               project=self._project)
149
150     @property
151     def log(self):
152         return self._log
153
154     def validate_args(self):
155         args = self._args
156         if args.upload_pkg is not None:
157             self._pkgs = args.upload_pkg
158             self.log.debug("Packages to upload: {}".format(self._pkgs))
159             if len(self._pkgs) == 0:
160                 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
161
162             for pkg in self._pkgs:
163                 self.log.debug("Check pkg: {}".format(pkg))
164                 if os.path.isfile(pkg) is False:
165                     raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
166
167         if args.instantiate:
168             if args.nsd_id is None:
169                 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
170
171             if args.datacenter:
172                 try:
173                     uuid.UUID(args.datacenter)
174                     self._dc = args.datacenter
175                 except ValueError as e:
176                     raise OnboardPkgInvalidDescId("Invalid UUID for datacenter {}: {}".
177                                                   format(args.datacenter, e))
178
179             elif args.vim_account:
180                 self._account = args.vim_account
181
182             else:
183                 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
184
185             self._service_name = args.instantiate
186             self._nsd_id = args.nsd_id
187
188             self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
189                                                                    self._service_name,
190                                                                    self._account))
191
192         if (self._pkgs is None) and (self._nsd_id is None) and (not args.list_nsds):
193             raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate or list options")
194
195         # Validate the port numbers are correct
196         def valid_port(port):
197             if 1 <= port <= 65535:
198                 return True
199             return False
200
201         if not valid_port(self._uport):
202             raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
203
204         if not valid_port(self._rport):
205             raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
206
207     def _exec_cmd(self, cmd):
208         self.log.debug("Execute command: {}".format(cmd))
209         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
210                                 stderr=subprocess.PIPE, shell=True)
211         (output, err) = proc.communicate()
212         rc = proc.returncode
213         self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
214                        format(rc, output, err))
215         if rc != 0:
216             raise OnboardPkgCmdError("Command {} failed ({}): {}".
217                                             format(cmd, rc, err))
218         return output.decode("utf-8")
219
220     def validate_connectivity(self):
221         if self._pkgs:
222             self.log.debug("Check connectivity to SO at {}:{}".
223                            format(self._ip, self._uport))
224
225             with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
226                 if sock.connect_ex((self._ip, self._uport)) != 0:
227                     raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
228                                                 format(self._ip, self._uport))
229             self.log.debug("Connection to SO upload port succeeded")
230
231         if self._nsd_id:
232             self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
233                            format(self._ip, self._rport, self._user, self._password))
234
235             rest_url = self._conf_url+"/resource-orchestrator"
236             try:
237                 output = self._exec_cmd(rest_url)
238                 self.log.debug("Output of restconf validation: {}".
239                                format(output))
240                 if len(output) != 0:
241                     js = json.loads(output)
242                     if "error" in js:
243                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
244                                                     format(js["error"]))
245
246                 self.log.debug("Connection to SO restconf port succeeded")
247
248             except OnboardPkgCmdError as e:
249                 self.log.error("SO restconf connect failed: {}".format(e))
250                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
251                                             format(e))
252
253
254     def _upload_package(self, pkg):
255         upload_cmd = "{url} -F \"package=@{pkg}\" ". \
256                                           format(url=self._onboard_url,
257                                                  pkg=pkg)
258         self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
259
260         output = self._exec_cmd(upload_cmd)
261
262         # Get the transaction id and wait for upload to complete
263         tx_id = json.loads(output)['transaction_id']
264
265         upload_status_url = "{url}/{id}/state". \
266                             format(url=self._upload_url,
267                                    id=tx_id)
268         status = ""
269         while status not in ['success', 'failure']:
270             output = self._exec_cmd(upload_status_url)
271             js = json.loads(output)
272             self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
273             status = js['status']
274
275         if status != 'success':
276             raise OnboardPkgUploadError("Package {} upload failed: {}".
277                                         format(pkg, js['errors']))
278
279         self.log.info("Upload of package {} succeeded".format(pkg))
280
281     def upload_packages(self):
282         if self._pkgs is None:
283             self.log.debug("Upload packages not provided")
284             return
285
286         for pkg in self._pkgs:
287             self._upload_package(pkg)
288
289     def instantiate(self):
290         if self._nsd_id is None:
291             self.log.debug("No NSD ID provided for instantiation")
292             return
293
294         # Check to see if datacenter is valid
295         if self._dc:
296             dc_url = "{url}/datacenters". format(url=self._oper_url)
297             output = self._exec_cmd(dc_url)
298             if (output is None) or (len(output) == 0):
299                 # Account not found
300                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
301                                         format(self._dc))
302             found = False
303             js = json.loads(output)
304             if "ro-accounts" in js["rw-launchpad:datacenters"]:
305                 for ro in js["rw-launchpad:datacenters"]["ro-accounts"]:
306                     if "datacenters" in ro:
307                         for dc in ro["datacenters"]:
308                             if dc["uuid"] == self._dc:
309                                 self.log.debug("Found datacenter {}".format(dc))
310                                 found = True
311                                 break
312                     if found:
313                         break
314
315             if found is False:
316                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
317                                         format(self._dc))
318
319
320         # Check cloud account is valid, if provided
321         if self._account:
322             acct_url = "{url}/cloud/account/{acct}". \
323                        format(url=self._conf_url, acct=self._account)
324             output = self._exec_cmd(acct_url)
325             if (output is None) or (len(output) == 0):
326                 # Account not found
327                 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
328                                           format(self._account))
329
330         # Check id NSD ID is valid
331         nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
332                   format(url=self._conf_url, nsd_id=self._nsd_id)
333         output = self._exec_cmd(nsd_url)
334         if (output is None) or (len(output) == 0):
335             # NSD not found
336             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
337                                      format(self._nsd_id))
338
339         js = json.loads(output)
340         if "error" in js:
341             raise OnboardPkgNsdError("NSD ID {} error: {}".
342                                      format(self._nsd_id,
343                                             js['error']))
344
345         try:
346             nsd = js['project-nsd:nsd']
347         except KeyError as e:
348             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
349                                      format(self._nsd_id))
350
351         self.log.debug("NSD to instantiate: {}".format(nsd))
352
353         # Generate a UUID for NS
354         ns_id = str(uuid.uuid4())
355         self.log.debug("NS instance uuid: {}".format(ns_id))
356
357         # Build the nsr post data
358         nsr = {"id": ns_id,
359                'name': self._service_name,
360                "nsd": nsd,}
361         if self._dc:
362             nsr['om-datacenter'] = self._dc
363         else:
364             nsr['cloud-account'] = self._account
365
366         data = {'nsr': [nsr]}
367
368         data_str = json.dumps(data)
369         self.log.debug("NSR post data: {}".format(data_str))
370
371         inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
372                    format(url=self._conf_url, data=data_str)
373         output = self._exec_cmd(inst_url)
374         self.log.debug("Instantiate output: {}".format(output))
375
376         js = json.loads(output)
377
378         if "last-error" in js:
379             msg = "Error instantiating NS as {} with NSD {}: ". \
380                   format(self._service_name, self._nsd_id,
381                          js["last-error"])
382             self.log.error(msg)
383             raise OnboardPkgInstError(msg)
384
385         elif "rpc-reply" in js:
386             reply = js["rpc-reply"]
387             if "rpc-error" in reply:
388                 msg = "Error instantiating NS as {} with NSD {}: ". \
389                   format(self._service_name, self._nsd_id,
390                          reply["rpc-error"])
391                 # self.log.error(msg)
392                 raise OnboardPkgInstError(msg)
393
394         self.log.info("Successfully initiated instantiation of NS as {} ({})".
395                       format(self._service_name, ns_id))
396
397     def list_nsds(self):
398         if self._args.list_nsds:
399             self.log.debug("Check NSDS at {}:{}, with credentials {}:{}".
400                            format(self._ip, self._rport, self._user, self._password))
401
402             rest_url = self._conf_url+"/nsd-catalog/nsd"
403             try:
404                 output = self._exec_cmd(rest_url)
405                 self.log.debug("Output of NSD list: {}".
406                                format(output))
407                 if output:
408                     js = json.loads(output)
409                     if "error" in js:
410                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
411                                                     format(js["error"]))
412                 else:
413                     print("No NSDs found on SO")
414                     return
415
416                 self.log.debug("NSD list: {}".format(js))
417                 print('List of NSDs on SO:\nName\tID')
418                 for nsd in js['project-nsd:nsd']:
419                     print('{}\t{}'.format(nsd['name'], nsd['id']))
420
421             except OnboardPkgCmdError as e:
422                 self.log.error("SO restconf connect failed: {}".format(e))
423                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
424                                             format(e))
425
426     def process(self):
427         try:
428             self.validate_args()
429         except Exception as e:
430             if args.verbose:
431                 log.exception(e)
432
433             print("\nERROR:", e)
434             print("\n")
435             parser.print_help()
436             sys.exit(2)
437
438         self.validate_connectivity()
439         self.upload_packages()
440         self.instantiate()
441         self.list_nsds()
442
443
444 if __name__ == "__main__":
445     parser = argparse.ArgumentParser(description='Upload and instantiate NS')
446     parser.add_argument("-s", "--so-ip", default='localhost',
447                         help="SO Launchpad IP")
448
449     parser.add_argument("-u", "--upload-pkg", action='append',
450                         help="Descriptor packages to upload. " + \
451                         "If multiple descriptors are provided, they are uploaded in the same sequence.")
452
453     parser.add_argument("-l", "--list-nsds", action='store_true',
454                         help="List available network service descriptors")
455
456     parser.add_argument("-i", "--instantiate",
457                         help="Instantiate a network service with the name")
458     parser.add_argument("-d", "--nsd-id",
459                         help="Network descriptor ID to instantiate")
460     parser.add_argument("-D", "--datacenter",
461                         help="OpenMano datacenter to instantiate on")
462     parser.add_argument("-c", "--vim-account",
463                         help="Cloud/VIM account to instantiate on")
464
465     parser.add_argument("--project", default='default',
466                         help="Project to use, default 'default'")
467     parser.add_argument("-o", "--onboard-port", default=8443, type=int,
468                         help="Onboarding port number - node port number, default 8443")
469     parser.add_argument("-p", "--upload-port", default=4567, type=int,
470                         help="Upload port number, default 4567")
471     parser.add_argument("-P", "--restconf-port", default=8008, type=int,
472                         help="RESTconf port number, default 8008")
473     parser.add_argument("--restconf-user", default='admin',
474                         help="RESTconf user name, default admin")
475     parser.add_argument("--restconf-password", default='admin',
476                         help="RESTconf password, default admin")
477
478     parser.add_argument("-v", "--verbose", action='store_true',
479                         help="Show more logs")
480
481     args = parser.parse_args()
482
483     fmt = logging.Formatter(
484         '%(asctime)-23s %(levelname)-5s  (%(name)s@%(process)d:' \
485         '%(filename)s:%(lineno)d) - %(message)s')
486     log = logging.getLogger('onboard-pkg')
487     log.setLevel(logging.INFO)
488     if args.verbose:
489         log.setLevel(logging.DEBUG)
490     ch = logging.StreamHandler()
491     ch.setLevel(logging.DEBUG)
492     ch.setFormatter(fmt)
493     log.addHandler(ch)
494
495     log.debug("Input arguments: {}".format(args))
496
497     try:
498         ob = OnboardPackage(log, args)
499         ob.process()
500     except Exception as e:
501         if args.verbose:
502             log.exception(e)
503
504         print("\nERROR:", e)
505         sys.exit(1)
506