741fbae2b58961d2d4a12d5cea8c510d335d78d6
[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
112         self._uport = args.upload_port
113         self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
114                             format(ip=self._ip,
115                                    port=self._uport)
116
117         self._rport = args.restconf_port
118         self._user = args.restconf_user
119         self._password = args.restconf_password
120         self._headers = '-H "accept: application/json"' + \
121                         ' -H "content-type: application/json"'
122         self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config". \
123                        format(header=self._headers,
124                               user=self._user,
125                               passwd=self._password,
126                               ip=self._ip,
127                               port=self._rport)
128         self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational". \
129                        format(header=self._headers,
130                               user=self._user,
131                               passwd=self._password,
132                               ip=self._ip,
133                               port=self._rport)
134
135     @property
136     def log(self):
137         return self._log
138
139     def validate_args(self):
140         if args.upload_pkg is not None:
141             self._pkgs = args.upload_pkg
142             self.log.debug("Packages to upload: {}".format(self._pkgs))
143             if len(self._pkgs) == 0:
144                 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
145
146             for pkg in self._pkgs:
147                 self.log.debug("Check pkg: {}".format(pkg))
148                 if os.path.isfile(pkg) is False:
149                     raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
150
151         if args.instantiate:
152             if args.nsd_id is None:
153                 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
154
155             if args.datacenter:
156                 try:
157                     uuid.UUID(args.datacenter)
158                     self._dc = args.datacenter
159                 except ValueError as e:
160                     raise OnboardPkgInvalidDescId("Invalid UUID for datacenter: {}".
161                                                   format(args.datacenter))
162
163             elif args.vim_account:
164                 self._account = args.vim_account
165
166             else:
167                 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
168
169             self._service_name = args.instantiate
170             self._nsd_id = args.nsd_id
171
172             self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
173                                                                    self._service_name,
174                                                                    self._account))
175
176         if (self._pkgs is None) and (self._nsd_id is None):
177             raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate options")
178
179         # Validate the port numbers are correct
180         def valid_port(port):
181             if 1 <= port <= 65535:
182                 return True
183             return False
184
185         if not valid_port(self._uport):
186             raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
187
188         if not valid_port(self._rport):
189             raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
190
191     def _exec_cmd(self, cmd):
192         self.log.debug("Execute command: {}".format(cmd))
193         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
194                                 stderr=subprocess.PIPE, shell=True)
195         (output, err) = proc.communicate()
196         rc = proc.returncode
197         self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
198                        format(rc, output, err))
199         if rc != 0:
200             raise OnboardPkgCmdError("Command {} failed ({}): {}".
201                                             format(cmd, rc, err))
202         return output.decode("utf-8")
203
204     def validate_connectivity(self):
205         if self._pkgs:
206             self.log.debug("Check connectivity to SO at {}:{}".
207                            format(self._ip, self._uport))
208
209             with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
210                 if sock.connect_ex((self._ip, self._uport)) != 0:
211                     raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
212                                                 format(self._ip, self._uport))
213             self.log.debug("Connection to SO upload port succeeded")
214
215         if self._nsd_id:
216             self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
217                            format(self._ip, self._rport, self._user, self._password))
218
219             rest_url = self._conf_url+"/resource-orchestrator"
220             try:
221                 output = self._exec_cmd(rest_url)
222                 self.log.debug("Output of restconf validation: {}".
223                                format(output))
224                 if len(output) != 0:
225                     js = json.loads(output)
226                     if "error" in js:
227                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
228                                                     format(js["error"]))
229
230                 self.log.debug("Connection to SO restconf port succeeded")
231
232             except OnboardPkgCmdError as e:
233                 self.log.error("SO restconf connect failed: {}".format(e))
234                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
235                                             format(e))
236
237
238     def _upload_package(self, pkg):
239         upload_cmd = "{url} -F \"descriptor=@{pkg}\" ". \
240                                           format(url=self._upload_url,
241                                                  pkg=pkg)
242         self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
243
244         output = self._exec_cmd(upload_cmd)
245
246         # Get the transaction id and wait for upload to complete
247         tx_id = json.loads(output)['transaction_id']
248
249         upload_status_url = "{url}/{id}/state". \
250                             format(url=self._upload_url,
251                                    id=tx_id)
252         status = ""
253         while status not in ['success', 'failure']:
254             output = self._exec_cmd(upload_status_url)
255             js = json.loads(output)
256             self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
257             status = js['status']
258
259         if status != 'success':
260             raise OnboardPkgUploadError("Package {} upload failed: {}".
261                                         format(pkg, js['errors']))
262
263         self.log.info("Upload of package {} succeeded".format(pkg))
264
265     def upload_packages(self):
266         if self._pkgs is None:
267             self.log.debug("Upload packages not provided")
268             return
269
270         for pkg in self._pkgs:
271             self._upload_package(pkg)
272
273     def instantiate(self):
274         if self._nsd_id is None:
275             self.log.debug("No NSD ID provided for instantiation")
276             return
277
278         # Check to see if datacenter is valid
279         if self._dc:
280             dc_url = "{url}/datacenters". format(url=self._oper_url)
281             output = self._exec_cmd(dc_url)
282             if (output is None) or (len(output) == 0):
283                 # Account not found
284                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
285                                         format(self._dc))
286             found = False
287             js = json.loads(output)
288             if "ro-accounts" in js["rw-launchpad:datacenters"]:
289                 for ro in js["rw-launchpad:datacenters"]["ro-accounts"]:
290                     if "datacenters" in ro:
291                         for dc in ro["datacenters"]:
292                             if dc["uuid"] == self._dc:
293                                 self.log.debug("Found datacenter {}".format(dc))
294                                 found = True
295                                 break
296                     if found:
297                         break
298
299             if found is False:
300                 raise OnboardPkgDcError("Datacenter {} provided is not valid".
301                                         format(self._dc))
302
303
304         # Check cloud account is valid, if provided
305         if self._account:
306             acct_url = "{url}/cloud/account/{acct}". \
307                        format(url=self._conf_url, acct=self._account)
308             output = self._exec_cmd(acct_url)
309             if (output is None) or (len(output) == 0):
310                 # Account not found
311                 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
312                                           format(self._account))
313
314         # Check id NSD ID is valid
315         nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
316                   format(url=self._conf_url, nsd_id=self._nsd_id)
317         output = self._exec_cmd(nsd_url)
318         if (output is None) or (len(output) == 0):
319             # NSD not found
320             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
321                                      format(self._nsd_id))
322
323         js = json.loads(output)
324         if "error" in js:
325             raise OnboardPkgNsdError("NSD ID {} error: {}".
326                                      format(self._nsd_id,
327                                             js['error']))
328
329         nsd = js['nsd:nsd']
330         self.log.debug("NSD to instantiate: {}".format(nsd))
331
332         # Generate a UUID for NS
333         ns_id = str(uuid.uuid4())
334         self.log.debug("NS instance uuid: {}".format(ns_id))
335
336         # Build the nsr post data
337         nsr = {"id": ns_id,
338                'name': self._service_name,
339                "nsd": nsd,}
340         if self._dc:
341             nsr['om-datacenter'] = self._dc
342         else:
343             nsr['cloud-account'] = self._account
344
345         data = {'nsr': [nsr]}
346
347         data_str = json.dumps(data)
348         self.log.debug("NSR post data: {}".format(data_str))
349
350         inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
351                    format(url=self._conf_url, data=data_str)
352         output = self._exec_cmd(inst_url)
353         self.log.debug("Instantiate output: {}".format(output))
354
355         js = json.loads(output)
356
357         if "last-error" in js:
358             msg = "Error instantiating NS as {} with NSD {}: ". \
359                   format(self._service_name, self._nsd_id,
360                          js["last-error"])
361             self.log.error(msg)
362             raise OnboardPkgInstError(msg)
363
364         elif "rpc-reply" in js:
365             reply = js["rpc-reply"]
366             if "rpc-error" in reply:
367                 msg = "Error instantiating NS as {} with NSD {}: ". \
368                   format(self._service_name, self._nsd_id,
369                          reply["rpc-error"])
370                 self.log.error(msg)
371                 raise OnboardPkgInstError(msg)
372
373         self.log.info("Successfully initiated instantiation of NS as {} ({})".
374                       format(self._service_name, ns_id))
375
376     def process(self):
377         self.validate_args()
378         self.validate_connectivity()
379         self.upload_packages()
380         self.instantiate()
381
382
383 if __name__ == "__main__":
384     parser = argparse.ArgumentParser(description='Upload and instantiate NS')
385     parser.add_argument("-s", "--so-ip", default='localhost',
386                         help="SO Launchpad IP")
387
388     parser.add_argument("-u", "--upload-pkg", action='append',
389                         help="Descriptor packages to upload. " + \
390                         "If multiple descriptors are provided, they are uploaded in the same sequence.")
391
392     parser.add_argument("-i", "--instantiate",
393                         help="Instantiate a network service with the name")
394     parser.add_argument("-d", "--nsd-id",
395                         help="Network descriptor ID to instantiate")
396     parser.add_argument("-D", "--datacenter",
397                         help="OpenMano datacenter to instantiate on")
398     parser.add_argument("-c", "--vim-account",
399                         help="Cloud/VIM account to instantiate on")
400
401     parser.add_argument("-p", "--upload-port", default=4567, type=int,
402                         help="Upload port number, default 4567")
403     parser.add_argument("-P", "--restconf-port", default=8008, type=int,
404                         help="RESTconf port number, default 8008")
405     parser.add_argument("--restconf-user", default='admin',
406                         help="RESTconf user name, default admin")
407     parser.add_argument("--restconf-password", default='admin',
408                         help="RESTconf password, default admin")
409
410     parser.add_argument("-v", "--verbose", action='store_true',
411                         help="Show more logs")
412
413     args = parser.parse_args()
414
415     fmt = logging.Formatter(
416         '%(asctime)-23s %(levelname)-5s  (%(name)s@%(process)d:' \
417         '%(filename)s:%(lineno)d) - %(message)s')
418     stderr_handler = logging.StreamHandler(stream=sys.stderr)
419     stderr_handler.setFormatter(fmt)
420     logging.basicConfig(level=logging.INFO)
421     log = logging.getLogger('onboard-pkg')
422     log.addHandler(stderr_handler)
423     if args.verbose:
424         log.setLevel(logging.DEBUG)
425
426     log.debug("Input arguments: {}".format(args))
427
428     ob = OnboardPackage(log, args)
429     ob.process()