2b2eb911c7b252a6bd04672b449a35b9395411ac
[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._pkgs = None
104
105         self._service_name = None
106         self._nsd_id = None
107         self._dc = None
108         self._account = None
109
110         self._ip = args.so_ip
111         self._api_server_ip = "localhost"
112
113         self._uport = args.upload_port
114         self._onboard_port = args.onboard_port
115         self._rport = args.restconf_port
116         self._user = args.restconf_user
117         self._password = args.restconf_password
118         self._onboard_url = "curl -k --user \"{user}:{passwd}\" \"https://{ip}:{port}/composer/upload?api_server=https://{api_server_ip}&upload_server=https://{ip}\"". \
119                              format(ip=self._ip,
120                                     port=self._onboard_port,
121                                     user=self._user,
122                                     passwd=self._password,
123                                     api_server_ip=self._api_server_ip)
124         self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
125                             format(ip=self._ip,
126                                    port=self._uport)
127
128         self._headers = '-H "accept: application/json"' + \
129                         ' -H "content-type: application/json"'
130         self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config". \
131                        format(header=self._headers,
132                               user=self._user,
133                               passwd=self._password,
134                               ip=self._ip,
135                               port=self._rport)
136         self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational". \
137                        format(header=self._headers,
138                               user=self._user,
139                               passwd=self._password,
140                               ip=self._ip,
141                               port=self._rport)
142
143     @property
144     def log(self):
145         return self._log
146
147     def validate_args(self):
148         if args.upload_pkg is not None:
149             self._pkgs = args.upload_pkg
150             self.log.debug("Packages to upload: {}".format(self._pkgs))
151             if len(self._pkgs) == 0:
152                 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
153
154             for pkg in self._pkgs:
155                 self.log.debug("Check pkg: {}".format(pkg))
156                 if os.path.isfile(pkg) is False:
157                     raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
158
159         if args.instantiate:
160             if args.nsd_id is None:
161                 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
162
163             if args.datacenter:
164                 try:
165                     uuid.UUID(args.datacenter)
166                     self._dc = args.datacenter
167                 except ValueError as e:
168                     raise OnboardPkgInvalidDescId("Invalid UUID for datacenter: {}".
169                                                   format(args.datacenter))
170
171             elif args.vim_account:
172                 self._account = args.vim_account
173
174             else:
175                 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
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._account))
183
184         if (self._pkgs is None) and (self._nsd_id is None):
185             raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate 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+"/resource-orchestrator"
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 to see if datacenter is valid
287         if self._dc:
288             dc_url = "{url}/datacenters". format(url=self._oper_url)
289             output = self._exec_cmd(dc_url)
290             if (output is None) or (len(output) == 0):
291                 # Account not found
292                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
293                                         format(self._dc))
294             found = False
295             js = json.loads(output)
296             if "ro-accounts" in js["rw-launchpad:datacenters"]:
297                 for ro in js["rw-launchpad:datacenters"]["ro-accounts"]:
298                     if "datacenters" in ro:
299                         for dc in ro["datacenters"]:
300                             if dc["uuid"] == self._dc:
301                                 self.log.debug("Found datacenter {}".format(dc))
302                                 found = True
303                                 break
304                     if found:
305                         break
306
307             if found is False:
308                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
309                                         format(self._dc))
310
311
312         # Check cloud account is valid, if provided
313         if self._account:
314             acct_url = "{url}/cloud/account/{acct}". \
315                        format(url=self._conf_url, acct=self._account)
316             output = self._exec_cmd(acct_url)
317             if (output is None) or (len(output) == 0):
318                 # Account not found
319                 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
320                                           format(self._account))
321
322         # Check id NSD ID is valid
323         nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
324                   format(url=self._conf_url, nsd_id=self._nsd_id)
325         output = self._exec_cmd(nsd_url)
326         if (output is None) or (len(output) == 0):
327             # NSD not found
328             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
329                                      format(self._nsd_id))
330
331         js = json.loads(output)
332         if "error" in js:
333             raise OnboardPkgNsdError("NSD ID {} error: {}".
334                                      format(self._nsd_id,
335                                             js['error']))
336
337         nsd = js['nsd:nsd']
338         self.log.debug("NSD to instantiate: {}".format(nsd))
339
340         # Generate a UUID for NS
341         ns_id = str(uuid.uuid4())
342         self.log.debug("NS instance uuid: {}".format(ns_id))
343
344         # Build the nsr post data
345         nsr = {"id": ns_id,
346                'name': self._service_name,
347                "nsd": nsd,}
348         if self._dc:
349             nsr['om-datacenter'] = self._dc
350         else:
351             nsr['cloud-account'] = self._account
352
353         data = {'nsr': [nsr]}
354
355         data_str = json.dumps(data)
356         self.log.debug("NSR post data: {}".format(data_str))
357
358         inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
359                    format(url=self._conf_url, data=data_str)
360         output = self._exec_cmd(inst_url)
361         self.log.debug("Instantiate output: {}".format(output))
362
363         js = json.loads(output)
364
365         if "last-error" in js:
366             msg = "Error instantiating NS as {} with NSD {}: ". \
367                   format(self._service_name, self._nsd_id,
368                          js["last-error"])
369             self.log.error(msg)
370             raise OnboardPkgInstError(msg)
371
372         elif "rpc-reply" in js:
373             reply = js["rpc-reply"]
374             if "rpc-error" in reply:
375                 msg = "Error instantiating NS as {} with NSD {}: ". \
376                   format(self._service_name, self._nsd_id,
377                          reply["rpc-error"])
378                 self.log.error(msg)
379                 raise OnboardPkgInstError(msg)
380
381         self.log.info("Successfully initiated instantiation of NS as {} ({})".
382                       format(self._service_name, ns_id))
383
384     def process(self):
385         self.validate_args()
386         self.validate_connectivity()
387         self.upload_packages()
388         self.instantiate()
389
390
391 if __name__ == "__main__":
392     parser = argparse.ArgumentParser(description='Upload and instantiate NS')
393     parser.add_argument("-s", "--so-ip", default='localhost',
394                         help="SO Launchpad IP")
395
396     parser.add_argument("-u", "--upload-pkg", action='append',
397                         help="Descriptor packages to upload. " + \
398                         "If multiple descriptors are provided, they are uploaded in the same sequence.")
399
400     parser.add_argument("-i", "--instantiate",
401                         help="Instantiate a network service with the name")
402     parser.add_argument("-d", "--nsd-id",
403                         help="Network descriptor ID to instantiate")
404     parser.add_argument("-D", "--datacenter",
405                         help="OpenMano datacenter to instantiate on")
406     parser.add_argument("-c", "--vim-account",
407                         help="Cloud/VIM account to instantiate on")
408
409     parser.add_argument("-o", "--onboard-port", default=8443, type=int,
410                         help="Onboarding port number - node port number, default 8443")
411     parser.add_argument("-p", "--upload-port", default=4567, type=int,
412                         help="Upload port number, default 4567")
413     parser.add_argument("-P", "--restconf-port", default=8008, type=int,
414                         help="RESTconf port number, default 8008")
415     parser.add_argument("--restconf-user", default='admin',
416                         help="RESTconf user name, default admin")
417     parser.add_argument("--restconf-password", default='admin',
418                         help="RESTconf password, default admin")
419
420     parser.add_argument("-v", "--verbose", action='store_true',
421                         help="Show more logs")
422
423     args = parser.parse_args()
424
425     fmt = logging.Formatter(
426         '%(asctime)-23s %(levelname)-5s  (%(name)s@%(process)d:' \
427         '%(filename)s:%(lineno)d) - %(message)s')
428     stderr_handler = logging.StreamHandler(stream=sys.stderr)
429     stderr_handler.setFormatter(fmt)
430     logging.basicConfig(level=logging.INFO)
431     log = logging.getLogger('onboard-pkg')
432     log.addHandler(stderr_handler)
433     if args.verbose:
434         log.setLevel(logging.DEBUG)
435
436     log.debug("Input arguments: {}".format(args))
437
438     ob = OnboardPackage(log, args)
439     ob.process()