update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[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._ro = 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                 self._dc = args.datacenter
173
174             if args.resource_orchestrator:
175                 self._ro = args.resource_orchestrator
176             
177             self._service_name = args.instantiate
178             self._nsd_id = args.nsd_id
179
180             self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
181                                                                    self._service_name,
182                                                                    self._dc))
183
184         if (self._pkgs is None) and (self._nsd_id is None) and (not args.list_nsds):
185             raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate or list options")
186
187         # Validate the port numbers are correct
188         def valid_port(port):
189             if 1 <= port <= 65535:
190                 return True
191             return False
192
193         if not valid_port(self._uport):
194             raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
195
196         if not valid_port(self._rport):
197             raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
198
199     def _exec_cmd(self, cmd):
200         self.log.debug("Execute command: {}".format(cmd))
201         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
202                                 stderr=subprocess.PIPE, shell=True)
203         (output, err) = proc.communicate()
204         rc = proc.returncode
205         self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
206                        format(rc, output, err))
207         if rc != 0:
208             raise OnboardPkgCmdError("Command {} failed ({}): {}".
209                                             format(cmd, rc, err))
210         return output.decode("utf-8")
211
212     def validate_connectivity(self):
213         if self._pkgs:
214             self.log.debug("Check connectivity to SO at {}:{}".
215                            format(self._ip, self._uport))
216
217             with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
218                 if sock.connect_ex((self._ip, self._uport)) != 0:
219                     raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
220                                                 format(self._ip, self._uport))
221             self.log.debug("Connection to SO upload port succeeded")
222
223         if self._nsd_id:
224             self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
225                            format(self._ip, self._rport, self._user, self._password))
226
227             rest_url = self._conf_url+"/ro-account"
228             try:
229                 output = self._exec_cmd(rest_url)
230                 self.log.debug("Output of restconf validation: {}".
231                                format(output))
232                 if len(output) != 0:
233                     js = json.loads(output)
234                     if "error" in js:
235                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
236                                                     format(js["error"]))
237
238                 self.log.debug("Connection to SO restconf port succeeded")
239
240             except OnboardPkgCmdError as e:
241                 self.log.error("SO restconf connect failed: {}".format(e))
242                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
243                                             format(e))
244
245
246     def _upload_package(self, pkg):
247         upload_cmd = "{url} -F \"package=@{pkg}\" ". \
248                                           format(url=self._onboard_url,
249                                                  pkg=pkg)
250         self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
251
252         output = self._exec_cmd(upload_cmd)
253
254         # Get the transaction id and wait for upload to complete
255         tx_id = json.loads(output)['transaction_id']
256
257         upload_status_url = "{url}/{id}/state". \
258                             format(url=self._upload_url,
259                                    id=tx_id)
260         status = ""
261         while status not in ['success', 'failure']:
262             output = self._exec_cmd(upload_status_url)
263             js = json.loads(output)
264             self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
265             status = js['status']
266
267         if status != 'success':
268             raise OnboardPkgUploadError("Package {} upload failed: {}".
269                                         format(pkg, js['errors']))
270
271         self.log.info("Upload of package {} succeeded".format(pkg))
272
273     def upload_packages(self):
274         if self._pkgs is None:
275             self.log.debug("Upload packages not provided")
276             return
277
278         for pkg in self._pkgs:
279             self._upload_package(pkg)
280
281     def instantiate(self):
282         if self._nsd_id is None:
283             self.log.debug("No NSD ID provided for instantiation")
284             return
285
286         # Check id NSD ID is valid
287         nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
288                   format(url=self._conf_url, nsd_id=self._nsd_id)
289         output = self._exec_cmd(nsd_url)
290         if (output is None) or (len(output) == 0):
291             # NSD not found
292             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
293                                      format(self._nsd_id))
294
295         js = json.loads(output)
296         if "error" in js:
297             raise OnboardPkgNsdError("NSD ID {} error: {}".
298                                      format(self._nsd_id,
299                                             js['error']))
300
301         try:
302             nsd = js['project-nsd:nsd']
303         except KeyError as e:
304             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
305                                      format(self._nsd_id))
306
307         self.log.debug("NSD to instantiate: {}".format(nsd))
308
309         # Generate a UUID for NS
310         ns_id = str(uuid.uuid4())
311         self.log.debug("NS instance uuid: {}".format(ns_id))
312
313         # Build the nsr post data
314         nsr = {"id": ns_id,
315                'name': self._service_name,
316                "nsd": nsd,}
317         if self._dc:
318             nsr['datacenter'] = self._dc
319          
320         if self._ro:
321             nsr['resource-orchestrator'] = self._ro  
322
323         data = {'nsr': [nsr]}
324
325         data_str = json.dumps(data)
326         self.log.debug("NSR post data: {}".format(data_str))
327
328         inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
329                    format(url=self._conf_url, data=data_str)
330         output = self._exec_cmd(inst_url)
331         self.log.debug("Instantiate output: {}".format(output))
332
333         js = json.loads(output)
334
335         if "last-error" in js:
336             msg = "Error instantiating NS as {} with NSD {}: ". \
337                   format(self._service_name, self._nsd_id,
338                          js["last-error"])
339             self.log.error(msg)
340             raise OnboardPkgInstError(msg)
341
342         elif "rpc-reply" in js:
343             reply = js["rpc-reply"]
344             if "rpc-error" in reply:
345                 msg = "Error instantiating NS as {} with NSD {}: ". \
346                   format(self._service_name, self._nsd_id,
347                          reply["rpc-error"])
348                 # self.log.error(msg)
349                 raise OnboardPkgInstError(msg)
350
351         self.log.info("Successfully initiated instantiation of NS as {} ({})".
352                       format(self._service_name, ns_id))
353
354     def list_nsds(self):
355         if self._args.list_nsds:
356             self.log.debug("Check NSDS at {}:{}, with credentials {}:{}".
357                            format(self._ip, self._rport, self._user, self._password))
358
359             rest_url = self._conf_url+"/nsd-catalog/nsd"
360             try:
361                 output = self._exec_cmd(rest_url)
362                 self.log.debug("Output of NSD list: {}".
363                                format(output))
364                 if output:
365                     js = json.loads(output)
366                     if "error" in js:
367                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
368                                                     format(js["error"]))
369                 else:
370                     print("No NSDs found on SO")
371                     return
372
373                 self.log.debug("NSD list: {}".format(js))
374                 print('List of NSDs on SO:\nName\tID')
375                 for nsd in js['project-nsd:nsd']:
376                     print('{}\t{}'.format(nsd['name'], nsd['id']))
377
378             except OnboardPkgCmdError as e:
379                 self.log.error("SO restconf connect failed: {}".format(e))
380                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
381                                             format(e))
382
383     def process(self):
384         try:
385             self.validate_args()
386         except Exception as e:
387             if args.verbose:
388                 log.exception(e)
389
390             print("\nERROR:", e)
391             print("\n")
392             parser.print_help()
393             sys.exit(2)
394
395         self.validate_connectivity()
396         self.upload_packages()
397         self.instantiate()
398         self.list_nsds()
399
400
401 if __name__ == "__main__":
402     parser = argparse.ArgumentParser(description='Upload and instantiate NS')
403     parser.add_argument("-s", "--so-ip", default='localhost',
404                         help="SO Launchpad IP")
405
406     parser.add_argument("-u", "--upload-pkg", action='append',
407                         help="Descriptor packages to upload. " + \
408                         "If multiple descriptors are provided, they are uploaded in the same sequence.")
409
410     parser.add_argument("-l", "--list-nsds", action='store_true',
411                         help="List available network service descriptors")
412
413     parser.add_argument("-i", "--instantiate",
414                         help="Instantiate a network service with the name")
415     parser.add_argument("-d", "--nsd-id",
416                         help="Network descriptor ID to instantiate")
417     parser.add_argument("-D", "--datacenter",
418                         help="OpenMano datacenter to instantiate on")
419     parser.add_argument("-r", "--resource-orchestrator",
420                         help="RO account to instantiate on")
421
422     parser.add_argument("--project", default='default',
423                         help="Project to use, default 'default'")
424     parser.add_argument("-o", "--onboard-port", default=8443, type=int,
425                         help="Onboarding port number - node port number, default 8443")
426     parser.add_argument("-p", "--upload-port", default=4567, type=int,
427                         help="Upload port number, default 4567")
428     parser.add_argument("-P", "--restconf-port", default=8008, type=int,
429                         help="RESTconf port number, default 8008")
430     parser.add_argument("--restconf-user", default='admin',
431                         help="RESTconf user name, default admin")
432     parser.add_argument("--restconf-password", default='admin',
433                         help="RESTconf password, default admin")
434
435     parser.add_argument("-v", "--verbose", action='store_true',
436                         help="Show more logs")
437
438     args = parser.parse_args()
439
440     fmt = logging.Formatter(
441         '%(asctime)-23s %(levelname)-5s  (%(name)s@%(process)d:' \
442         '%(filename)s:%(lineno)d) - %(message)s')
443     log = logging.getLogger('onboard-pkg')
444     log.setLevel(logging.INFO)
445     if args.verbose:
446         log.setLevel(logging.DEBUG)
447     ch = logging.StreamHandler()
448     ch.setLevel(logging.DEBUG)
449     ch.setFormatter(fmt)
450     log.addHandler(ch)
451
452     log.debug("Input arguments: {}".format(args))
453
454     try:
455         ob = OnboardPackage(log, args)
456         ob.process()
457     except Exception as e:
458         if args.verbose:
459             log.exception(e)
460
461         print("\nERROR:", e)
462         sys.exit(1)
463