Expose new node group attributes in CLI and RAPI
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API version 2 baserlib.library.
23
24   PUT or POST?
25   ============
26
27   According to RFC2616 the main difference between PUT and POST is that
28   POST can create new resources but PUT can only create the resource the
29   URI was pointing to on the PUT request.
30
31   To be in context of this module for instance creation POST on
32   /2/instances is legitim while PUT would be not, due to it does create a
33   new entity and not just replace /2/instances with it.
34
35   So when adding new methods, if they are operating on the URI entity itself,
36   PUT should be prefered over POST.
37
38 """
39
40 # pylint: disable-msg=C0103
41
42 # C0103: Invalid name, since the R_* names are not conforming
43
44 from ganeti import opcodes
45 from ganeti import http
46 from ganeti import constants
47 from ganeti import cli
48 from ganeti import utils
49 from ganeti import rapi
50 from ganeti.rapi import baserlib
51
52
53 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
54 I_FIELDS = ["name", "admin_state", "os",
55             "pnode", "snodes",
56             "disk_template",
57             "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
58             "network_port",
59             "disk.sizes", "disk_usage",
60             "beparams", "hvparams",
61             "oper_state", "oper_ram", "oper_vcpus", "status",
62             "custom_hvparams", "custom_beparams", "custom_nicparams",
63             ] + _COMMON_FIELDS
64
65 N_FIELDS = ["name", "offline", "master_candidate", "drained",
66             "dtotal", "dfree",
67             "mtotal", "mnode", "mfree",
68             "pinst_cnt", "sinst_cnt",
69             "ctotal", "cnodes", "csockets",
70             "pip", "sip", "role",
71             "pinst_list", "sinst_list",
72             "master_capable", "vm_capable",
73             "group.uuid",
74             ] + _COMMON_FIELDS
75
76 G_FIELDS = ["name", "uuid",
77             "node_cnt", "node_list",
78             "ctime", "mtime", "serial_no",
79             ]  # "tags" is missing to be able to use _COMMON_FIELDS here.
80
81 _NR_DRAINED = "drained"
82 _NR_MASTER_CANDIATE = "master-candidate"
83 _NR_MASTER = "master"
84 _NR_OFFLINE = "offline"
85 _NR_REGULAR = "regular"
86
87 _NR_MAP = {
88   "M": _NR_MASTER,
89   "C": _NR_MASTER_CANDIATE,
90   "D": _NR_DRAINED,
91   "O": _NR_OFFLINE,
92   "R": _NR_REGULAR,
93   }
94
95 # Request data version field
96 _REQ_DATA_VERSION = "__version__"
97
98 # Feature string for instance creation request data version 1
99 _INST_CREATE_REQV1 = "instance-create-reqv1"
100
101 # Feature string for instance reinstall request version 1
102 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
103
104 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
105 _WFJC_TIMEOUT = 10
106
107
108 class R_version(baserlib.R_Generic):
109   """/version resource.
110
111   This resource should be used to determine the remote API version and
112   to adapt clients accordingly.
113
114   """
115   @staticmethod
116   def GET():
117     """Returns the remote API version.
118
119     """
120     return constants.RAPI_VERSION
121
122
123 class R_2_info(baserlib.R_Generic):
124   """Cluster info.
125
126   """
127   @staticmethod
128   def GET():
129     """Returns cluster information.
130
131     """
132     client = baserlib.GetClient()
133     return client.QueryClusterInfo()
134
135
136 class R_2_features(baserlib.R_Generic):
137   """/2/features resource.
138
139   """
140   @staticmethod
141   def GET():
142     """Returns list of optional RAPI features implemented.
143
144     """
145     return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1]
146
147
148 class R_2_os(baserlib.R_Generic):
149   """/2/os resource.
150
151   """
152   @staticmethod
153   def GET():
154     """Return a list of all OSes.
155
156     Can return error 500 in case of a problem.
157
158     Example: ["debian-etch"]
159
160     """
161     cl = baserlib.GetClient()
162     op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
163     job_id = baserlib.SubmitJob([op], cl)
164     # we use custom feedback function, instead of print we log the status
165     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
166     diagnose_data = result[0]
167
168     if not isinstance(diagnose_data, list):
169       raise http.HttpBadGateway(message="Can't get OS list")
170
171     os_names = []
172     for (name, variants) in diagnose_data:
173       os_names.extend(cli.CalculateOSNames(name, variants))
174
175     return os_names
176
177
178 class R_2_redist_config(baserlib.R_Generic):
179   """/2/redistribute-config resource.
180
181   """
182   @staticmethod
183   def PUT():
184     """Redistribute configuration to all nodes.
185
186     """
187     return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
188
189
190 class R_2_jobs(baserlib.R_Generic):
191   """/2/jobs resource.
192
193   """
194   @staticmethod
195   def GET():
196     """Returns a dictionary of jobs.
197
198     @return: a dictionary with jobs id and uri.
199
200     """
201     fields = ["id"]
202     cl = baserlib.GetClient()
203     # Convert the list of lists to the list of ids
204     result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
205     return baserlib.BuildUriList(result, "/2/jobs/%s",
206                                  uri_fields=("id", "uri"))
207
208
209 class R_2_jobs_id(baserlib.R_Generic):
210   """/2/jobs/[job_id] resource.
211
212   """
213   def GET(self):
214     """Returns a job status.
215
216     @return: a dictionary with job parameters.
217         The result includes:
218             - id: job ID as a number
219             - status: current job status as a string
220             - ops: involved OpCodes as a list of dictionaries for each
221               opcodes in the job
222             - opstatus: OpCodes status as a list
223             - opresult: OpCodes results as a list of lists
224
225     """
226     fields = ["id", "ops", "status", "summary",
227               "opstatus", "opresult", "oplog",
228               "received_ts", "start_ts", "end_ts",
229               ]
230     job_id = self.items[0]
231     result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
232     if result is None:
233       raise http.HttpNotFound()
234     return baserlib.MapFields(fields, result)
235
236   def DELETE(self):
237     """Cancel not-yet-started job.
238
239     """
240     job_id = self.items[0]
241     result = baserlib.GetClient().CancelJob(job_id)
242     return result
243
244
245 class R_2_jobs_id_wait(baserlib.R_Generic):
246   """/2/jobs/[job_id]/wait resource.
247
248   """
249   # WaitForJobChange provides access to sensitive information and blocks
250   # machine resources (it's a blocking RAPI call), hence restricting access.
251   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
252
253   def GET(self):
254     """Waits for job changes.
255
256     """
257     job_id = self.items[0]
258
259     fields = self.getBodyParameter("fields")
260     prev_job_info = self.getBodyParameter("previous_job_info", None)
261     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
262
263     if not isinstance(fields, list):
264       raise http.HttpBadRequest("The 'fields' parameter should be a list")
265
266     if not (prev_job_info is None or isinstance(prev_job_info, list)):
267       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
268                                 " be a list")
269
270     if not (prev_log_serial is None or
271             isinstance(prev_log_serial, (int, long))):
272       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
273                                 " be a number")
274
275     client = baserlib.GetClient()
276     result = client.WaitForJobChangeOnce(job_id, fields,
277                                          prev_job_info, prev_log_serial,
278                                          timeout=_WFJC_TIMEOUT)
279     if not result:
280       raise http.HttpNotFound()
281
282     if result == constants.JOB_NOTCHANGED:
283       # No changes
284       return None
285
286     (job_info, log_entries) = result
287
288     return {
289       "job_info": job_info,
290       "log_entries": log_entries,
291       }
292
293
294 class R_2_nodes(baserlib.R_Generic):
295   """/2/nodes resource.
296
297   """
298   def GET(self):
299     """Returns a list of all nodes.
300
301     """
302     client = baserlib.GetClient()
303
304     if self.useBulk():
305       bulkdata = client.QueryNodes([], N_FIELDS, False)
306       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
307     else:
308       nodesdata = client.QueryNodes([], ["name"], False)
309       nodeslist = [row[0] for row in nodesdata]
310       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
311                                    uri_fields=("id", "uri"))
312
313
314 class R_2_nodes_name(baserlib.R_Generic):
315   """/2/nodes/[node_name] resources.
316
317   """
318   def GET(self):
319     """Send information about a node.
320
321     """
322     node_name = self.items[0]
323     client = baserlib.GetClient()
324
325     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
326                                             names=[node_name], fields=N_FIELDS,
327                                             use_locking=self.useLocking())
328
329     return baserlib.MapFields(N_FIELDS, result[0])
330
331
332 class R_2_nodes_name_role(baserlib.R_Generic):
333   """ /2/nodes/[node_name]/role resource.
334
335   """
336   def GET(self):
337     """Returns the current node role.
338
339     @return: Node role
340
341     """
342     node_name = self.items[0]
343     client = baserlib.GetClient()
344     result = client.QueryNodes(names=[node_name], fields=["role"],
345                                use_locking=self.useLocking())
346
347     return _NR_MAP[result[0][0]]
348
349   def PUT(self):
350     """Sets the node role.
351
352     @return: a job id
353
354     """
355     if not isinstance(self.request_body, basestring):
356       raise http.HttpBadRequest("Invalid body contents, not a string")
357
358     node_name = self.items[0]
359     role = self.request_body
360
361     if role == _NR_REGULAR:
362       candidate = False
363       offline = False
364       drained = False
365
366     elif role == _NR_MASTER_CANDIATE:
367       candidate = True
368       offline = drained = None
369
370     elif role == _NR_DRAINED:
371       drained = True
372       candidate = offline = None
373
374     elif role == _NR_OFFLINE:
375       offline = True
376       candidate = drained = None
377
378     else:
379       raise http.HttpBadRequest("Can't set '%s' role" % role)
380
381     op = opcodes.OpSetNodeParams(node_name=node_name,
382                                  master_candidate=candidate,
383                                  offline=offline,
384                                  drained=drained,
385                                  force=bool(self.useForce()))
386
387     return baserlib.SubmitJob([op])
388
389
390 class R_2_nodes_name_evacuate(baserlib.R_Generic):
391   """/2/nodes/[node_name]/evacuate resource.
392
393   """
394   def POST(self):
395     """Evacuate all secondary instances off a node.
396
397     """
398     node_name = self.items[0]
399     remote_node = self._checkStringVariable("remote_node", default=None)
400     iallocator = self._checkStringVariable("iallocator", default=None)
401     early_r = bool(self._checkIntVariable("early_release", default=0))
402     dry_run = bool(self.dryRun())
403
404     cl = baserlib.GetClient()
405
406     op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
407                                           iallocator=iallocator,
408                                           remote_node=remote_node)
409
410     job_id = baserlib.SubmitJob([op], cl)
411     # we use custom feedback function, instead of print we log the status
412     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
413
414     jobs = []
415     for iname, node in result:
416       if dry_run:
417         jid = None
418       else:
419         op = opcodes.OpReplaceDisks(instance_name=iname,
420                                     remote_node=node, disks=[],
421                                     mode=constants.REPLACE_DISK_CHG,
422                                     early_release=early_r)
423         jid = baserlib.SubmitJob([op])
424       jobs.append((jid, iname, node))
425
426     return jobs
427
428
429 class R_2_nodes_name_migrate(baserlib.R_Generic):
430   """/2/nodes/[node_name]/migrate resource.
431
432   """
433   def POST(self):
434     """Migrate all primary instances from a node.
435
436     """
437     node_name = self.items[0]
438
439     if "live" in self.queryargs and "mode" in self.queryargs:
440       raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
441                                 " be passed")
442     elif "live" in self.queryargs:
443       if self._checkIntVariable("live", default=1):
444         mode = constants.HT_MIGRATION_LIVE
445       else:
446         mode = constants.HT_MIGRATION_NONLIVE
447     else:
448       mode = self._checkStringVariable("mode", default=None)
449
450     op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
451
452     return baserlib.SubmitJob([op])
453
454
455 class R_2_nodes_name_storage(baserlib.R_Generic):
456   """/2/nodes/[node_name]/storage ressource.
457
458   """
459   # LUQueryNodeStorage acquires locks, hence restricting access to GET
460   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
461
462   def GET(self):
463     node_name = self.items[0]
464
465     storage_type = self._checkStringVariable("storage_type", None)
466     if not storage_type:
467       raise http.HttpBadRequest("Missing the required 'storage_type'"
468                                 " parameter")
469
470     output_fields = self._checkStringVariable("output_fields", None)
471     if not output_fields:
472       raise http.HttpBadRequest("Missing the required 'output_fields'"
473                                 " parameter")
474
475     op = opcodes.OpQueryNodeStorage(nodes=[node_name],
476                                     storage_type=storage_type,
477                                     output_fields=output_fields.split(","))
478     return baserlib.SubmitJob([op])
479
480
481 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
482   """/2/nodes/[node_name]/storage/modify ressource.
483
484   """
485   def PUT(self):
486     node_name = self.items[0]
487
488     storage_type = self._checkStringVariable("storage_type", None)
489     if not storage_type:
490       raise http.HttpBadRequest("Missing the required 'storage_type'"
491                                 " parameter")
492
493     name = self._checkStringVariable("name", None)
494     if not name:
495       raise http.HttpBadRequest("Missing the required 'name'"
496                                 " parameter")
497
498     changes = {}
499
500     if "allocatable" in self.queryargs:
501       changes[constants.SF_ALLOCATABLE] = \
502         bool(self._checkIntVariable("allocatable", default=1))
503
504     op = opcodes.OpModifyNodeStorage(node_name=node_name,
505                                      storage_type=storage_type,
506                                      name=name,
507                                      changes=changes)
508     return baserlib.SubmitJob([op])
509
510
511 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
512   """/2/nodes/[node_name]/storage/repair ressource.
513
514   """
515   def PUT(self):
516     node_name = self.items[0]
517
518     storage_type = self._checkStringVariable("storage_type", None)
519     if not storage_type:
520       raise http.HttpBadRequest("Missing the required 'storage_type'"
521                                 " parameter")
522
523     name = self._checkStringVariable("name", None)
524     if not name:
525       raise http.HttpBadRequest("Missing the required 'name'"
526                                 " parameter")
527
528     op = opcodes.OpRepairNodeStorage(node_name=node_name,
529                                      storage_type=storage_type,
530                                      name=name)
531     return baserlib.SubmitJob([op])
532
533
534 class R_2_groups(baserlib.R_Generic):
535   """/2/groups resource.
536
537   """
538   def GET(self):
539     """Returns a list of all node groups.
540
541     """
542     client = baserlib.GetClient()
543
544     if self.useBulk():
545       bulkdata = client.QueryGroups([], G_FIELDS, False)
546       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
547     else:
548       data = client.QueryGroups([], ["name"], False)
549       groupnames = [row[0] for row in data]
550       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
551                                    uri_fields=("name", "uri"))
552
553
554 class R_2_groups_name(baserlib.R_Generic):
555   """/2/groups/[group_name] resources.
556
557   """
558   def GET(self):
559     """Send information about a node group.
560
561     """
562     group_name = self.items[0]
563     client = baserlib.GetClient()
564
565     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
566                                             names=[group_name], fields=G_FIELDS,
567                                             use_locking=self.useLocking())
568
569     return baserlib.MapFields(G_FIELDS, result[0])
570
571
572
573 def _ParseInstanceCreateRequestVersion1(data, dry_run):
574   """Parses an instance creation request version 1.
575
576   @rtype: L{opcodes.OpCreateInstance}
577   @return: Instance creation opcode
578
579   """
580   # Disks
581   disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
582
583   disks = []
584   for idx, i in enumerate(disks_input):
585     baserlib.CheckType(i, dict, "Disk %d specification" % idx)
586
587     # Size is mandatory
588     try:
589       size = i[constants.IDISK_SIZE]
590     except KeyError:
591       raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
592                                 " size" % idx)
593
594     disk = {
595       constants.IDISK_SIZE: size,
596       }
597
598     # Optional disk access mode
599     try:
600       disk_access = i[constants.IDISK_MODE]
601     except KeyError:
602       pass
603     else:
604       disk[constants.IDISK_MODE] = disk_access
605
606     disks.append(disk)
607
608   assert len(disks_input) == len(disks)
609
610   # Network interfaces
611   nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
612
613   nics = []
614   for idx, i in enumerate(nics_input):
615     baserlib.CheckType(i, dict, "NIC %d specification" % idx)
616
617     nic = {}
618
619     for field in constants.INIC_PARAMS:
620       try:
621         value = i[field]
622       except KeyError:
623         continue
624
625       nic[field] = value
626
627     nics.append(nic)
628
629   assert len(nics_input) == len(nics)
630
631   # HV/BE parameters
632   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
633   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
634
635   beparams = baserlib.CheckParameter(data, "beparams", default={})
636   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
637
638   return opcodes.OpCreateInstance(
639     mode=baserlib.CheckParameter(data, "mode"),
640     instance_name=baserlib.CheckParameter(data, "name"),
641     os_type=baserlib.CheckParameter(data, "os"),
642     osparams=baserlib.CheckParameter(data, "osparams", default={}),
643     force_variant=baserlib.CheckParameter(data, "force_variant",
644                                           default=False),
645     no_install=baserlib.CheckParameter(data, "no_install", default=False),
646     pnode=baserlib.CheckParameter(data, "pnode", default=None),
647     snode=baserlib.CheckParameter(data, "snode", default=None),
648     disk_template=baserlib.CheckParameter(data, "disk_template"),
649     disks=disks,
650     nics=nics,
651     src_node=baserlib.CheckParameter(data, "src_node", default=None),
652     src_path=baserlib.CheckParameter(data, "src_path", default=None),
653     start=baserlib.CheckParameter(data, "start", default=True),
654     wait_for_sync=True,
655     ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
656     name_check=baserlib.CheckParameter(data, "name_check", default=True),
657     file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
658                                              default=None),
659     file_driver=baserlib.CheckParameter(data, "file_driver",
660                                         default=constants.FD_LOOP),
661     source_handshake=baserlib.CheckParameter(data, "source_handshake",
662                                              default=None),
663     source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
664                                            default=None),
665     source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
666                                                  default=None),
667     iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
668     hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
669     hvparams=hvparams,
670     beparams=beparams,
671     dry_run=dry_run,
672     )
673
674
675 class R_2_instances(baserlib.R_Generic):
676   """/2/instances resource.
677
678   """
679   def GET(self):
680     """Returns a list of all available instances.
681
682     """
683     client = baserlib.GetClient()
684
685     use_locking = self.useLocking()
686     if self.useBulk():
687       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
688       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
689     else:
690       instancesdata = client.QueryInstances([], ["name"], use_locking)
691       instanceslist = [row[0] for row in instancesdata]
692       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
693                                    uri_fields=("id", "uri"))
694
695   def _ParseVersion0CreateRequest(self):
696     """Parses an instance creation request version 0.
697
698     Request data version 0 is deprecated and should not be used anymore.
699
700     @rtype: L{opcodes.OpCreateInstance}
701     @return: Instance creation opcode
702
703     """
704     # Do not modify anymore, request data version 0 is deprecated
705     beparams = baserlib.MakeParamsDict(self.request_body,
706                                        constants.BES_PARAMETERS)
707     hvparams = baserlib.MakeParamsDict(self.request_body,
708                                        constants.HVS_PARAMETERS)
709     fn = self.getBodyParameter
710
711     # disk processing
712     disk_data = fn('disks')
713     if not isinstance(disk_data, list):
714       raise http.HttpBadRequest("The 'disks' parameter should be a list")
715     disks = []
716     for idx, d in enumerate(disk_data):
717       if not isinstance(d, int):
718         raise http.HttpBadRequest("Disk %d specification wrong: should"
719                                   " be an integer" % idx)
720       disks.append({"size": d})
721
722     # nic processing (one nic only)
723     nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
724     if fn("ip", None) is not None:
725       nics[0]["ip"] = fn("ip")
726     if fn("mode", None) is not None:
727       nics[0]["mode"] = fn("mode")
728     if fn("link", None) is not None:
729       nics[0]["link"] = fn("link")
730     if fn("bridge", None) is not None:
731       nics[0]["bridge"] = fn("bridge")
732
733     # Do not modify anymore, request data version 0 is deprecated
734     return opcodes.OpCreateInstance(
735       mode=constants.INSTANCE_CREATE,
736       instance_name=fn('name'),
737       disks=disks,
738       disk_template=fn('disk_template'),
739       os_type=fn('os'),
740       pnode=fn('pnode', None),
741       snode=fn('snode', None),
742       iallocator=fn('iallocator', None),
743       nics=nics,
744       start=fn('start', True),
745       ip_check=fn('ip_check', True),
746       name_check=fn('name_check', True),
747       wait_for_sync=True,
748       hypervisor=fn('hypervisor', None),
749       hvparams=hvparams,
750       beparams=beparams,
751       file_storage_dir=fn('file_storage_dir', None),
752       file_driver=fn('file_driver', constants.FD_LOOP),
753       dry_run=bool(self.dryRun()),
754       )
755
756   def POST(self):
757     """Create an instance.
758
759     @return: a job id
760
761     """
762     if not isinstance(self.request_body, dict):
763       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
764
765     # Default to request data version 0
766     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
767
768     if data_version == 0:
769       op = self._ParseVersion0CreateRequest()
770     elif data_version == 1:
771       op = _ParseInstanceCreateRequestVersion1(self.request_body,
772                                                self.dryRun())
773     else:
774       raise http.HttpBadRequest("Unsupported request data version %s" %
775                                 data_version)
776
777     return baserlib.SubmitJob([op])
778
779
780 class R_2_instances_name(baserlib.R_Generic):
781   """/2/instances/[instance_name] resources.
782
783   """
784   def GET(self):
785     """Send information about an instance.
786
787     """
788     client = baserlib.GetClient()
789     instance_name = self.items[0]
790
791     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
792                                             names=[instance_name],
793                                             fields=I_FIELDS,
794                                             use_locking=self.useLocking())
795
796     return baserlib.MapFields(I_FIELDS, result[0])
797
798   def DELETE(self):
799     """Delete an instance.
800
801     """
802     op = opcodes.OpRemoveInstance(instance_name=self.items[0],
803                                   ignore_failures=False,
804                                   dry_run=bool(self.dryRun()))
805     return baserlib.SubmitJob([op])
806
807
808 class R_2_instances_name_info(baserlib.R_Generic):
809   """/2/instances/[instance_name]/info resource.
810
811   """
812   def GET(self):
813     """Request detailed instance information.
814
815     """
816     instance_name = self.items[0]
817     static = bool(self._checkIntVariable("static", default=0))
818
819     op = opcodes.OpQueryInstanceData(instances=[instance_name],
820                                      static=static)
821     return baserlib.SubmitJob([op])
822
823
824 class R_2_instances_name_reboot(baserlib.R_Generic):
825   """/2/instances/[instance_name]/reboot resource.
826
827   Implements an instance reboot.
828
829   """
830   def POST(self):
831     """Reboot an instance.
832
833     The URI takes type=[hard|soft|full] and
834     ignore_secondaries=[False|True] parameters.
835
836     """
837     instance_name = self.items[0]
838     reboot_type = self.queryargs.get('type',
839                                      [constants.INSTANCE_REBOOT_HARD])[0]
840     ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
841     op = opcodes.OpRebootInstance(instance_name=instance_name,
842                                   reboot_type=reboot_type,
843                                   ignore_secondaries=ignore_secondaries,
844                                   dry_run=bool(self.dryRun()))
845
846     return baserlib.SubmitJob([op])
847
848
849 class R_2_instances_name_startup(baserlib.R_Generic):
850   """/2/instances/[instance_name]/startup resource.
851
852   Implements an instance startup.
853
854   """
855   def PUT(self):
856     """Startup an instance.
857
858     The URI takes force=[False|True] parameter to start the instance
859     if even if secondary disks are failing.
860
861     """
862     instance_name = self.items[0]
863     force_startup = bool(self._checkIntVariable('force'))
864     op = opcodes.OpStartupInstance(instance_name=instance_name,
865                                    force=force_startup,
866                                    dry_run=bool(self.dryRun()))
867
868     return baserlib.SubmitJob([op])
869
870
871 class R_2_instances_name_shutdown(baserlib.R_Generic):
872   """/2/instances/[instance_name]/shutdown resource.
873
874   Implements an instance shutdown.
875
876   """
877   def PUT(self):
878     """Shutdown an instance.
879
880     """
881     instance_name = self.items[0]
882     op = opcodes.OpShutdownInstance(instance_name=instance_name,
883                                     dry_run=bool(self.dryRun()))
884
885     return baserlib.SubmitJob([op])
886
887
888 def _ParseInstanceReinstallRequest(name, data):
889   """Parses a request for reinstalling an instance.
890
891   """
892   if not isinstance(data, dict):
893     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
894
895   ostype = baserlib.CheckParameter(data, "os")
896   start = baserlib.CheckParameter(data, "start", exptype=bool,
897                                   default=True)
898   osparams = baserlib.CheckParameter(data, "osparams", default=None)
899
900   ops = [
901     opcodes.OpShutdownInstance(instance_name=name),
902     opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
903                                 osparams=osparams),
904     ]
905
906   if start:
907     ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
908
909   return ops
910
911
912 class R_2_instances_name_reinstall(baserlib.R_Generic):
913   """/2/instances/[instance_name]/reinstall resource.
914
915   Implements an instance reinstall.
916
917   """
918   def POST(self):
919     """Reinstall an instance.
920
921     The URI takes os=name and nostartup=[0|1] optional
922     parameters. By default, the instance will be started
923     automatically.
924
925     """
926     if self.request_body:
927       if self.queryargs:
928         raise http.HttpBadRequest("Can't combine query and body parameters")
929
930       body = self.request_body
931     else:
932       if not self.queryargs:
933         raise http.HttpBadRequest("Missing query parameters")
934       # Legacy interface, do not modify/extend
935       body = {
936         "os": self._checkStringVariable("os"),
937         "start": not self._checkIntVariable("nostartup"),
938         }
939
940     ops = _ParseInstanceReinstallRequest(self.items[0], body)
941
942     return baserlib.SubmitJob(ops)
943
944
945 class R_2_instances_name_replace_disks(baserlib.R_Generic):
946   """/2/instances/[instance_name]/replace-disks resource.
947
948   """
949   def POST(self):
950     """Replaces disks on an instance.
951
952     """
953     instance_name = self.items[0]
954     remote_node = self._checkStringVariable("remote_node", default=None)
955     mode = self._checkStringVariable("mode", default=None)
956     raw_disks = self._checkStringVariable("disks", default=None)
957     iallocator = self._checkStringVariable("iallocator", default=None)
958
959     if raw_disks:
960       try:
961         disks = [int(part) for part in raw_disks.split(",")]
962       except ValueError, err:
963         raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
964     else:
965       disks = []
966
967     op = opcodes.OpReplaceDisks(instance_name=instance_name,
968                                 remote_node=remote_node,
969                                 mode=mode,
970                                 disks=disks,
971                                 iallocator=iallocator)
972
973     return baserlib.SubmitJob([op])
974
975
976 class R_2_instances_name_activate_disks(baserlib.R_Generic):
977   """/2/instances/[instance_name]/activate-disks resource.
978
979   """
980   def PUT(self):
981     """Activate disks for an instance.
982
983     The URI might contain ignore_size to ignore current recorded size.
984
985     """
986     instance_name = self.items[0]
987     ignore_size = bool(self._checkIntVariable('ignore_size'))
988
989     op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
990                                          ignore_size=ignore_size)
991
992     return baserlib.SubmitJob([op])
993
994
995 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
996   """/2/instances/[instance_name]/deactivate-disks resource.
997
998   """
999   def PUT(self):
1000     """Deactivate disks for an instance.
1001
1002     """
1003     instance_name = self.items[0]
1004
1005     op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
1006
1007     return baserlib.SubmitJob([op])
1008
1009
1010 class R_2_instances_name_prepare_export(baserlib.R_Generic):
1011   """/2/instances/[instance_name]/prepare-export resource.
1012
1013   """
1014   def PUT(self):
1015     """Prepares an export for an instance.
1016
1017     @return: a job id
1018
1019     """
1020     instance_name = self.items[0]
1021     mode = self._checkStringVariable("mode")
1022
1023     op = opcodes.OpPrepareExport(instance_name=instance_name,
1024                                  mode=mode)
1025
1026     return baserlib.SubmitJob([op])
1027
1028
1029 def _ParseExportInstanceRequest(name, data):
1030   """Parses a request for an instance export.
1031
1032   @rtype: L{opcodes.OpExportInstance}
1033   @return: Instance export opcode
1034
1035   """
1036   mode = baserlib.CheckParameter(data, "mode",
1037                                  default=constants.EXPORT_MODE_LOCAL)
1038   target_node = baserlib.CheckParameter(data, "destination")
1039   shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
1040   remove_instance = baserlib.CheckParameter(data, "remove_instance",
1041                                             exptype=bool, default=False)
1042   x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
1043   destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1044                                                 default=None)
1045
1046   return opcodes.OpExportInstance(instance_name=name,
1047                                   mode=mode,
1048                                   target_node=target_node,
1049                                   shutdown=shutdown,
1050                                   remove_instance=remove_instance,
1051                                   x509_key_name=x509_key_name,
1052                                   destination_x509_ca=destination_x509_ca)
1053
1054
1055 class R_2_instances_name_export(baserlib.R_Generic):
1056   """/2/instances/[instance_name]/export resource.
1057
1058   """
1059   def PUT(self):
1060     """Exports an instance.
1061
1062     @return: a job id
1063
1064     """
1065     if not isinstance(self.request_body, dict):
1066       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1067
1068     op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1069
1070     return baserlib.SubmitJob([op])
1071
1072
1073 def _ParseMigrateInstanceRequest(name, data):
1074   """Parses a request for an instance migration.
1075
1076   @rtype: L{opcodes.OpMigrateInstance}
1077   @return: Instance migration opcode
1078
1079   """
1080   mode = baserlib.CheckParameter(data, "mode", default=None)
1081   cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1082                                     default=False)
1083
1084   return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1085                                    cleanup=cleanup)
1086
1087
1088 class R_2_instances_name_migrate(baserlib.R_Generic):
1089   """/2/instances/[instance_name]/migrate resource.
1090
1091   """
1092   def PUT(self):
1093     """Migrates an instance.
1094
1095     @return: a job id
1096
1097     """
1098     baserlib.CheckType(self.request_body, dict, "Body contents")
1099
1100     op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1101
1102     return baserlib.SubmitJob([op])
1103
1104
1105 def _ParseRenameInstanceRequest(name, data):
1106   """Parses a request for renaming an instance.
1107
1108   @rtype: L{opcodes.OpRenameInstance}
1109   @return: Instance rename opcode
1110
1111   """
1112   new_name = baserlib.CheckParameter(data, "new_name")
1113   ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1114   name_check = baserlib.CheckParameter(data, "name_check", default=True)
1115
1116   return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1117                                   name_check=name_check, ip_check=ip_check)
1118
1119
1120 class R_2_instances_name_rename(baserlib.R_Generic):
1121   """/2/instances/[instance_name]/rename resource.
1122
1123   """
1124   def PUT(self):
1125     """Changes the name of an instance.
1126
1127     @return: a job id
1128
1129     """
1130     baserlib.CheckType(self.request_body, dict, "Body contents")
1131
1132     op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1133
1134     return baserlib.SubmitJob([op])
1135
1136
1137 def _ParseModifyInstanceRequest(name, data):
1138   """Parses a request for modifying an instance.
1139
1140   @rtype: L{opcodes.OpSetInstanceParams}
1141   @return: Instance modify opcode
1142
1143   """
1144   osparams = baserlib.CheckParameter(data, "osparams", default={})
1145   force = baserlib.CheckParameter(data, "force", default=False)
1146   nics = baserlib.CheckParameter(data, "nics", default=[])
1147   disks = baserlib.CheckParameter(data, "disks", default=[])
1148   disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1149   remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1150   os_name = baserlib.CheckParameter(data, "os_name", default=None)
1151   force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1152
1153   # HV/BE parameters
1154   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1155   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1156                       allowed_values=[constants.VALUE_DEFAULT])
1157
1158   beparams = baserlib.CheckParameter(data, "beparams", default={})
1159   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1160                       allowed_values=[constants.VALUE_DEFAULT])
1161
1162   return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1163                                      beparams=beparams, osparams=osparams,
1164                                      force=force, nics=nics, disks=disks,
1165                                      disk_template=disk_template,
1166                                      remote_node=remote_node, os_name=os_name,
1167                                      force_variant=force_variant)
1168
1169
1170 class R_2_instances_name_modify(baserlib.R_Generic):
1171   """/2/instances/[instance_name]/modify resource.
1172
1173   """
1174   def PUT(self):
1175     """Changes some parameters of an instance.
1176
1177     @return: a job id
1178
1179     """
1180     baserlib.CheckType(self.request_body, dict, "Body contents")
1181
1182     op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1183
1184     return baserlib.SubmitJob([op])
1185
1186
1187 class _R_Tags(baserlib.R_Generic):
1188   """ Quasiclass for tagging resources
1189
1190   Manages tags. When inheriting this class you must define the
1191   TAG_LEVEL for it.
1192
1193   """
1194   TAG_LEVEL = None
1195
1196   def __init__(self, items, queryargs, req):
1197     """A tag resource constructor.
1198
1199     We have to override the default to sort out cluster naming case.
1200
1201     """
1202     baserlib.R_Generic.__init__(self, items, queryargs, req)
1203
1204     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1205       self.name = None
1206     else:
1207       self.name = items[0]
1208
1209   def GET(self):
1210     """Returns a list of tags.
1211
1212     Example: ["tag1", "tag2", "tag3"]
1213
1214     """
1215     # pylint: disable-msg=W0212
1216     return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1217
1218   def PUT(self):
1219     """Add a set of tags.
1220
1221     The request as a list of strings should be PUT to this URI. And
1222     you'll have back a job id.
1223
1224     """
1225     # pylint: disable-msg=W0212
1226     if 'tag' not in self.queryargs:
1227       raise http.HttpBadRequest("Please specify tag(s) to add using the"
1228                                 " the 'tag' parameter")
1229     return baserlib._Tags_PUT(self.TAG_LEVEL,
1230                               self.queryargs['tag'], name=self.name,
1231                               dry_run=bool(self.dryRun()))
1232
1233   def DELETE(self):
1234     """Delete a tag.
1235
1236     In order to delete a set of tags, the DELETE
1237     request should be addressed to URI like:
1238     /tags?tag=[tag]&tag=[tag]
1239
1240     """
1241     # pylint: disable-msg=W0212
1242     if 'tag' not in self.queryargs:
1243       # no we not gonna delete all tags
1244       raise http.HttpBadRequest("Cannot delete all tags - please specify"
1245                                 " tag(s) using the 'tag' parameter")
1246     return baserlib._Tags_DELETE(self.TAG_LEVEL,
1247                                  self.queryargs['tag'],
1248                                  name=self.name,
1249                                  dry_run=bool(self.dryRun()))
1250
1251
1252 class R_2_instances_name_tags(_R_Tags):
1253   """ /2/instances/[instance_name]/tags resource.
1254
1255   Manages per-instance tags.
1256
1257   """
1258   TAG_LEVEL = constants.TAG_INSTANCE
1259
1260
1261 class R_2_nodes_name_tags(_R_Tags):
1262   """ /2/nodes/[node_name]/tags resource.
1263
1264   Manages per-node tags.
1265
1266   """
1267   TAG_LEVEL = constants.TAG_NODE
1268
1269
1270 class R_2_tags(_R_Tags):
1271   """ /2/instances/tags resource.
1272
1273   Manages cluster tags.
1274
1275   """
1276   TAG_LEVEL = constants.TAG_CLUSTER