Przeglądaj źródła

AMBARI-1769. Python REST client to invoke REST calls. (Subin M via mahadev)

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/trunk@1467879 13f79535-47bb-0310-9956-ffa450edef68
Mahadev Konar 12 lat temu
rodzic
commit
58dabba136

+ 2 - 0
CHANGES.txt

@@ -1466,6 +1466,8 @@ AMBARI-1.2.0 branch:
  AMBARI-1202. Unncessary use of xml tree python library in ambari-server
  setup. Its not being used. (Siddharth Wagle via mahadev)
 
+ AMBARI-1769. Python REST client to invoke REST calls. (Subin M via mahadev)
+
  IMPROVEMENTS
 
  BUG FIXES

+ 169 - 0
ambari-client/pom.xml

@@ -0,0 +1,169 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+  <parent>
+    <groupId>org.apache.ambari</groupId>
+    <artifactId>ambari-project</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <relativePath>../ambari-project</relativePath>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.ambari</groupId>
+  <artifactId>ambari-client</artifactId>
+  <packaging>pom</packaging>
+  <version>1.3.0-SNAPSHOT</version>
+  <name>Ambari client</name>
+  <description>Ambari Python client</description>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <final.name>${project.artifactId}-${project.version}</final.name>
+    <package.release>1</package.release>
+    <package.prefix>/usr</package.prefix>
+    <package.log.dir>/var/log/ambari-client</package.log.dir>
+    <package.pid.dir>/var/run/ambari-client</package.pid.dir>
+    <skipTests>false</skipTests>
+    <install.dir>/usr/lib/python2.6/site-packages/ambari_client</install.dir>
+    <lib.dir>/usr/lib/ambari-client/lib</lib.dir>
+  </properties>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.0</version>
+      </plugin>
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <configuration>
+          <tarLongFileMode>gnu</tarLongFileMode>
+          <descriptors>
+            <descriptor>src/packages/tarball/all.xml</descriptor>
+          </descriptors>
+        </configuration>
+        <executions>
+          <execution>
+            <id>build-tarball</id>
+            <phase>prepare-package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.2</version>
+        <executions>
+          <execution>
+            <configuration>
+              <executable>python2.6</executable>
+              <workingDirectory>src/test/python</workingDirectory>
+              <arguments>
+                <argument>unitTests.py</argument>
+              </arguments>
+              <environmentVariables>
+                <PYTHONPATH>${project.basedir}/../ambari-common/src/test/python:${project.basedir}/src/main/python/ambari_client:$PYTHONPATH</PYTHONPATH>
+              </environmentVariables>
+              <skip>${skipTests}</skip>
+            </configuration>
+            <id>python-test</id>
+            <phase>test</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+          <execution>
+            <configuration>
+              <executable>python2.6</executable>
+              <workingDirectory>target/ambari-client-${project.version}</workingDirectory>
+              <arguments>
+                <argument>${project.basedir}/src/main/python/setup.py</argument>
+                <argument>clean</argument>
+                <argument>bdist_dumb</argument>
+              </arguments>
+              <environmentVariables>
+                <PYTHONPATH>target/ambari-client-${project.version}:$PYTHONPATH</PYTHONPATH>
+              </environmentVariables>
+            </configuration>
+            <id>python-package</id>
+            <phase>package</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>rpm-maven-plugin</artifactId>
+        <version>2.1-alpha-2</version>
+        <executions>
+          <execution>
+            <phase>none</phase>
+            <goals>
+              <goal>rpm</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <copyright>2012, Apache Software Foundation</copyright>
+          <group>Development</group>
+          <description>Maven Recipe: RPM Package.</description>
+          <requires>
+            <require>openssl</require>
+            <require>zlib</require>
+            <require>${python.ver}</require>
+          </requires>
+          <needarch>x86_64</needarch>
+          <autoRequires>false</autoRequires>
+          <mappings>
+          <mapping>
+              <directory>${install.dir}</directory>
+              <sources>
+                <source>
+                  <location>${project.build.directory}/${project.artifactId}-${project.version}/ambari_client</location>
+                </source>
+              </sources>
+            </mapping>
+            <!-- -->
+          </mappings>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <configuration>
+          <excludes>
+            <exclude>src/examples/*</exclude>
+            <exclude>src/test/python/dummy*.txt</exclude>
+            <exclude>src/main/python/ambari_client/imports.txt</exclude>
+            <exclude>src/main/puppet/modules/stdlib/**</exclude>
+            <exclude>**/*.erb</exclude>
+            <exclude>**/*.json</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+    </plugins>
+    <extensions>
+      <extension>
+        <groupId>org.apache.maven.wagon</groupId>
+        <artifactId>wagon-ssh-external</artifactId>
+      </extension>
+    </extensions>
+  </build>
+</project>

+ 84 - 0
ambari-client/src/examples/example.py

@@ -0,0 +1,84 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import os 
+import sys 
+import logging
+from ambari_client.ambari_api import  AmbariClient 
+
+
+def main():
+  """
+  This method has few examples on how to use the ambari_client api
+  """
+    path = os.getcwd() ;
+    print path
+    sys.path.append(path)
+    
+    logging.basicConfig(filename="ambari_client.log", level=logging.DEBUG ,filemode="w")
+    logging.info("Program started")
+     
+     
+     
+    client = AmbariClient("localhost", 8080, "admin","admin",version=1)
+
+    all_clusters = client.get_all_clusters()
+    print all_clusters
+    print all_clusters.to_json_dict()
+    print"\n"
+    
+    cluster = client.get_cluster('test1')
+    print cluster
+    print"\n"
+    
+#    serviceList = cluster.get_all_services()
+#    print serviceList
+#    print"\n"
+#    
+#    
+#    for service in serviceList:
+#        print str(service.service_name)+" = "+str(service.state)
+#    print"\n"    
+
+    ganglia = cluster.get_service("GANGLIA")       
+    print  ganglia.state
+    print"\n"
+    
+    
+    
+#    cluster_ref =  ganglia.clusterRef
+#    print cluster_ref.cluster_name
+#    print"\n"
+    
+    
+    ganglia.stop()
+#    ganglia.start()
+       
+
+    
+########################################################################
+#
+# The "main" entry
+#
+########################################################################
+if __name__ == '__main__':
+    main()
+######################################################################## 

+ 98 - 0
ambari-client/src/main/python/ambari_client/ambari_api.py

@@ -0,0 +1,98 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import logging
+from ambari_client.http_client import HttpClient
+from ambari_client.resources import  clusters
+from ambari_client.rest_resource import RestResource
+
+__docformat__ = "epytext"
+
+LOG = logging.getLogger(__name__)
+
+
+API_VERSION = 1
+
+
+
+
+class AmbariClient(RestResource):
+  """
+  AmbariClient top-level root resources.
+  """
+
+  def __init__(self, host_name, port=None,user_name="admin", password="admin",
+               version=API_VERSION):
+    """
+    Creates a RestResource object.
+
+    @param host_name: The hostname  server.
+    @param port: The port of the server. 
+    @param user_name: Login name.
+    @param password: Login password.
+    @param version: API version.
+    @return RestResource object referring to the root.
+    """
+    
+    self._version = version
+    protocol = "http"
+    if port is None: 
+      port = 8080
+    host_url = "%s://%s:%s/api/v%s" %(protocol, host_name, port, version)
+    client = HttpClient(host_url, user_name , password )
+    RestResource.__init__(self, client)
+
+
+
+  @property
+  def version(self):
+    """
+    Returns the API version .
+    """
+    return self._version
+
+
+  def get_all_clusters(self):
+    """
+    Get all clusters.
+    @return: A list of ModelList.
+    """
+    return clusters.get_all_clusters(self)
+
+  def get_cluster(self, cluster_name):
+    """
+    Get a cluster by cluster_name.
+
+    @param cluster_name Cluster cluster_name.
+    @return An ClusterModel.
+    """
+    return clusters.get_cluster(self, cluster_name)
+
+  
+
+
+
+def get_root_resource(server_host, server_port=None,username="admin", password="admin",
+                       version=1):
+  """
+   AmbariClient.
+  """
+  return AmbariClient(server_host, server_port, username, password, version)

+ 166 - 0
ambari-client/src/main/python/ambari_client/http_client.py

@@ -0,0 +1,166 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import logging
+import posixpath
+import sys
+import pycurl
+import cStringIO
+import StringIO
+import pdb
+try:
+  import json
+except ImportError:
+  import simplejson as json
+from ambari_client.http_utils import uri_encoding
+
+__docformat__ = "epytext"
+
+LOG = logging.getLogger(__name__)
+
+ 
+class HttpClient(object):
+  """
+  Basic HTTP client for rest APIs.
+  """
+  def __init__(self, host_url, user_name , password ):
+    """
+    @param host_url: The base url to the API.
+
+    """
+
+    self._host_url = host_url.rstrip('/')
+    self._headers = { }
+    self.c = pycurl.Curl()
+    if user_name is not None:
+        self.c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
+        userpass = user_name + ':'
+        if password is not None: 
+            userpass += password
+    LOG.debug( "pycurl.USERPWD value = "+str(userpass))
+    self.c.setopt(pycurl.USERPWD, userpass)
+
+
+  def set_headers(self, headers):
+    """
+    Add headers to the request
+    """
+    self._headers = headers
+    return self
+
+  @property
+  def host_url(self):
+    return self._host_url
+
+  def _get_headers(self, headers):
+    res = self._headers.copy()
+    if headers:
+      res.update(headers)
+    return res
+
+  def invoke(self, http_method, path, payload=None, headers=None):
+    """
+    Submit an HTTP request.
+    @param http_method: GET, POST, PUT, DELETE
+    @param path: The path of the resource.
+    @param payload: The payload to attach to the body of the request.
+    @param headers: The headers to set for this request.
+
+    @return: The result of REST request
+    """
+    #pdb.set_trace()
+    LOG.debug ("invoke : http_method = "+str(http_method))
+    # Prepare URL and params
+    url = self._normalize(path)
+    if http_method in ("GET", "DELETE"):
+      if payload is not None:
+        self.logger.warn(
+            "GET http_method does not pass any payload. Path '%s'" % (path,))
+        payload = None
+
+
+    buf = cStringIO.StringIO()
+    self.c.setopt(pycurl.WRITEFUNCTION, buf.write)
+    LOG.debug ("invoke : url = "+str(url))
+    # set http_method
+    if http_method == "GET":
+        self.c.setopt(pycurl.HTTPGET, 1)
+    elif http_method == "HEAD":
+        self.c.setopt(pycurl.HTTPGET, 1)
+        self.c.setopt(pycurl.NOBODY, 1)
+    elif http_method == "POST":
+        self.c.setopt(pycurl.POST, 1)
+    elif http_method == "PUT":
+        self.c.setopt(pycurl.UPLOAD, 1)
+    else:
+        self.c.setopt(pycurl.CUSTOMREQUEST, http_method)
+        iri_to_uri
+    if http_method in ('POST','PUT'):
+      LOG.debug( "data..........."+str(payload))
+      data = json.dumps(payload)
+      data= data.decode('unicode-escape')
+      LOG.debug( data)
+      data = self._to_bytestring(data)
+      LOG.debug( data)
+      content = StringIO.StringIO(data)
+      LOG.debug( content)
+      content_length = len(data)
+      LOG.debug( "content_length........."+str(content_length))
+
+      if http_method == 'POST':
+        self.c.setopt(pycurl.POSTFIELDSIZE, content_length)
+      else:
+        self.c.setopt(pycurl.INFILESIZE, content_length)
+      self.c.setopt(pycurl.READFUNCTION, content.read)
+      
+      
+    self.c.setopt(self.c.URL, url)
+    headers = self._get_headers(headers)
+    self.c.setopt(pycurl.HTTPHEADER,
+                        ["%s: %s" % pair for pair in sorted(headers.iteritems())])
+
+    LOG.debug ("invoke : pycurl.EFFECTIVE_URL = "+self.c.getinfo(pycurl.EFFECTIVE_URL))
+    try:
+        self.c.perform()
+    except Exception, ex:
+        LOG.debug (sys.stderr, str(ex))
+        raise ex
+    contents_type= self.c.getinfo(pycurl.CONTENT_TYPE)
+    LOG.debug ("invoke : pycurl.CONTENT_TYPE = "+contents_type)
+    code = self.c.getinfo(pycurl.RESPONSE_CODE)
+    LOG.debug ("invoke : pycurl.RESPONSE_CODE = "+str(code))
+    response = buf.getvalue()
+    buf.close()
+    LOG.debug ("invoke : COMPLETED ")
+    return response , code , contents_type
+
+  def _to_bytestring(self ,s):
+#    if not isinstance(s, basestring):
+#      raise TypeError("value should be a str or unicode")
+    if isinstance(s, unicode):
+      return s.encode('utf-8')
+    return s
+
+  def _normalize(self, path):
+    res = self._host_url
+    if path:
+      res += posixpath.normpath('/' + path.lstrip('/'))
+    return uri_encoding(res)

+ 53 - 0
ambari-client/src/main/python/ambari_client/http_utils.py

@@ -0,0 +1,53 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+
+import types
+import urllib
+import urllib2
+
+
+def uri_encoding(url):
+    """
+    Returns an ASCII string version of the URL.
+    """
+    if url is None:
+        return url
+    return urllib.quote(get_utf8_str(url), safe="/#%[]=:;$&()+,!?*@'~")
+
+def get_utf8_str(strr, encoding='utf-8'):
+    """
+    Returns a utf8 ecoded 'str'.
+    """
+    errors='strict'
+    if not isinstance(strr, basestring):
+        try:
+            return str(strr)
+        except UnicodeEncodeError:
+            if isinstance(strr, Exception):
+                return ' '.join([get_utf8_str(arg, encoding) for arg in strr])
+            return unicode(strr).encode(encoding, errors)
+    elif strr and encoding != 'utf-8':
+        return strr.decode('utf-8', errors).encode(encoding, errors)
+    elif isinstance(strr, unicode):
+        return strr.encode(encoding, errors)
+    else:
+        return strr

+ 115 - 0
ambari-client/src/main/python/ambari_client/model/base_model.py

@@ -0,0 +1,115 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+'''
+
+import sys
+import logging
+import time
+from  ambari_client.model.utils  import get_REF_object,get_unicode,getREF_var_name
+
+
+__docformat__ = "epytext"
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseModel(object):
+  """
+  The BaseModel 
+
+  RW_ATTR - A list of mutable attributes
+  RO_ATTR - A list of immutable attributes
+  REF_ATTR - A REF attribute   
+
+  """
+  RO_ATTR = ( )         
+  RW_ATTR = ( )         
+  REF_ATTR = ( )        
+
+  
+  def __init__(self, resource_root, **rw_attrs):
+    #print" ================== base_model\n"
+    #print locals()
+    self._resource_root = resource_root
+    for k, v in rw_attrs.items():
+      if k not in self.RW_ATTR:
+        raise ValueError("Unknown argument '%s' in %s" %
+                         (k, self.__class__.__name__))
+      self._setattr(k, v)
+
+  def _get_resource_root(self):
+    return self._resource_root
+
+  def to_json_dict(self):
+    dic = { }
+    for attr in self.RW_ATTR:
+      value = getattr(self, attr)
+      try:
+        value = value.to_json_dict()
+      except Exception:
+        pass
+      dic[attr] = value
+    return dic
+
+  def _setattr(self, k, v):
+    """Set an attribute. """
+    value=v
+    if v and k.endswith("Ref"):
+      cls_name =  k[0].upper() + k[1:] 
+      cls_name = cls_name[:-3] + "ModelRef"
+      cls=get_REF_object(cls_name)
+      LOG.debug(str(cls_name) +"  -  "+str(cls))
+      v = get_unicode(v)
+      var_name=getREF_var_name(cls_name)
+      c={str(var_name):str(v)}
+      LOG.debug(c)
+      value = cls(self._get_resource_root(), **c)
+    setattr(self, k, value)
+
+
+
+
+
+
+class ModelList(object):
+  """A list of Model objects"""
+  LIST_KEY = "items"
+
+  def __init__(self, objects):
+    self.objects = objects
+
+  def __str__(self):
+    return "<<ModelList>>[size = %d]) = [%s]" % (
+        len(self.objects),
+        ", ".join([str(item) for item in self.objects]))
+
+  def to_json_dict(self):
+    return { ModelList.LIST_KEY :
+            [ x.to_json_dict() for x in self.objects ] }
+
+  def __len__(self):
+    return self.objects.__len__()
+
+  def __iter__(self):
+    return self.objects.__iter__()
+
+  def __getitem__(self, i):
+    return self.objects.__getitem__(i)
+

+ 90 - 0
ambari-client/src/main/python/ambari_client/model/cluster.py

@@ -0,0 +1,90 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import logging
+from ambari_client.model.base_model import  BaseModel 
+from ambari_client.model.paths import CLUSTERS_PATH 
+from ambari_client.model import service
+from ambari_client.model.utils import ModelUtils ,retain_self_helper
+
+
+LOG = logging.getLogger(__name__)
+
+
+def get_cluster(resource_root, cluster_name):
+  """
+  Lookup a cluster by cluster_name
+  @param resource_root: The root Resource .
+  @param cluster_name: cluster_name
+  @return: A ClusterModel object
+  """
+  dic = resource_root.get("%s/%s" % (CLUSTERS_PATH, cluster_name))
+  return ModelUtils.create_model(ClusterModel ,dic, resource_root,"Clusters")
+
+def get_all_clusters(root_resource, details=None):
+  """
+  Get all clusters
+  @param root_resource: The root Resource .
+  @return: A list of ClusterModel objects.
+  """
+  dic = root_resource.get(CLUSTERS_PATH)
+  return ModelUtils.get_model_list(ClusterModel, dic, root_resource ,"Clusters")
+
+
+
+
+    
+    
+class ClusterModel(BaseModel):
+
+  RW_ATTR = ('cluster_name', 'version')
+ 
+  
+  def __init__(self, resource_root, cluster_name, version):
+    retain_self_helper(**locals())
+
+  def __str__(self):
+    return "<<ClusterModel>> = %s; version = %s" % (self.cluster_name, self.version)
+
+  def _path(self):
+    return "%s/%s" % (CLUSTERS_PATH, self.cluster_name)
+
+  def get_service(self, service_name):
+    """
+    Get a service by service_name.
+    @param service_name: Service service_name
+    @return: A ServiceModel object
+    """
+    return service.get_service(self._get_resource_root(), service_name, self.cluster_name)
+
+  def get_all_services(self, detail = None):
+    """
+    Get all services in this cluster.
+    @return: A list of ServiceModel objects.
+    """
+    return service.get_all_services(self._get_resource_root(), self.cluster_name)
+
+
+
+class ClusterModelRef(BaseModel):
+  RW_ATTR = ('cluster_name',)
+  def __init__(self, resource_root, cluster_name = None):
+    retain_self_helper(**locals())

+ 26 - 0
ambari-client/src/main/python/ambari_client/model/paths.py

@@ -0,0 +1,26 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+CLUSTERS_PATH = "/clusters"
+SERVICES_PATH = "/clusters/%s/services"
+SERVICE_PATH = "/clusters/%s/services/%s"
+
+

+ 93 - 0
ambari-client/src/main/python/ambari_client/model/service.py

@@ -0,0 +1,93 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import logging
+
+from ambari_client.model.base_model import  BaseModel 
+from ambari_client.model.paths import SERVICES_PATH ,SERVICE_PATH 
+from ambari_client.model.utils import ModelUtils ,retain_self_helper
+
+LOG = logging.getLogger(__name__)
+
+
+def get_all_services(resource_root, cluster_name="default"):
+  path = SERVICES_PATH % (cluster_name,)
+  path = path +'?fields=*'
+  dic = resource_root.get(path)
+  return ModelUtils.get_model_list(ServiceModel, dic, resource_root ,"ServiceInfo")
+
+
+def get_service(resource_root, service_name, cluster_name="default"):
+  path = "%s/%s" % (SERVICES_PATH % (cluster_name,), service_name)
+  dic = resource_root.get(path)
+  return ModelUtils.create_model(ServiceModel ,dic, resource_root,"ServiceInfo") 
+
+
+    
+    
+    
+class ServiceModel(BaseModel):
+  RO_ATTR = ('state',  'cluster_name')
+  RW_ATTR = ('service_name', 'type')
+  REF_ATTR = ('cluster_name',)
+
+  def __init__(self, resource_root, service_name):
+    #BaseModel.__init__(self, **locals())
+    retain_self_helper(**locals())
+
+  def __str__(self):
+    return "<<ServiceModel>> = %s (cluster_name = %s)" % (self.service_name, self._get_cluster_name())
+
+  def _get_cluster_name(self):
+    if self.clusterRef:
+      return self.clusterRef.cluster_name
+    return None
+
+  def _path(self):
+    """
+    Return the API path for this service.
+    """
+    if self._get_cluster_name():
+      return SERVICE_PATH % (self._get_cluster_name(), self.service_name)
+    else:
+      return ''
+
+
+  def _action(self, data=None):
+    path = self._path() 
+    self._get_resource_root().put(path, payload=data)
+    return None
+
+
+  def start(self):
+    """
+    Start a service.
+    """
+    data={"ServiceInfo": {"state": "STARTED"}}
+    return self._action(data)
+
+  def stop(self):
+    """
+    Stop a service.
+    """
+    data={"ServiceInfo": {"state": "INSTALLED"}}
+    return self._action(data)
+
+

+ 135 - 0
ambari-client/src/main/python/ambari_client/model/utils.py

@@ -0,0 +1,135 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+import logging
+import sys
+
+LOG = logging.getLogger(__name__)
+
+ref_dic = {"cluster_name":"clusterRef"}
+ref_class_dic = {"ClusterModelRef":"cluster_name"}
+ref_pkg_dic={"ClusterModelRef":"ambari_client.model.cluster"}
+
+class ModelUtils(object):
+
+  @staticmethod
+  def get_model_list(member_cls, dic, resource_root , RESOURCE_KEY_WORD):
+    #print locals()
+    from ambari_client.model.base_model import  ModelList
+    json_list = dic[ModelList.LIST_KEY]
+    LOG.debug ("get_model_list : getting the "+str(ModelList.LIST_KEY)+ " value---> \n\t"+str(json_list) )
+    
+    if isinstance(json_list, list):
+        json_list_new = [ x.get(RESOURCE_KEY_WORD) for x in json_list]
+    
+    LOG.debug ("get_model_list: creating a array for "+str(RESOURCE_KEY_WORD)+" value---> \n\t"+str(json_list_new))
+    objects = [ ModelUtils.create_model(member_cls,x, resource_root ,RESOURCE_KEY_WORD) for x in json_list_new ]
+    LOG.debug (objects)
+    return ModelList(objects)
+
+  @staticmethod
+  def create_model(member_cls, dic, resource_root,RESOURCE_KEY_WORD):
+    #print locals()
+    rw_dict = { }
+    LOG.debug ("    create_model : dic =   "+str(dic))
+    if isinstance(dic, dict) and dic.has_key(RESOURCE_KEY_WORD):
+        dic=dic[RESOURCE_KEY_WORD]
+        LOG.debug ("    dic.items() 2   =   "+str(dic.items()))
+    for k, v in dic.items():
+      LOG.debug (k)
+      LOG.debug (v)
+      if k in member_cls.RW_ATTR:
+        LOG.debug (k + " is there in RW_ATTR")
+        rw_dict[k] = v
+        del dic[k]
+
+    rw_dict = get_unicode_kw(rw_dict)
+    obj = member_cls(resource_root, **rw_dict)
+
+
+    for attr in member_cls.RO_ATTR:
+      obj._setattr(attr, None)
+
+
+    for k, v in dic.items():
+      if k in member_cls.RO_ATTR:
+        obj._setattr(k, v)
+      else:
+        LOG.debug("Unexpected attribute '%s' in %s json" %(k, member_cls.__name__))
+
+    for attr in member_cls.REF_ATTR:
+      LOG.debug(attr)
+      obj._setattr(getREF_class_name(attr), None)
+
+
+    for k, v in dic.items():
+      if k in member_cls.REF_ATTR:
+        obj._setattr(getREF_class_name(k), v)
+      else:
+        LOG.debug("Unexpected attribute '%s' in %s json" %(k, member_cls.__name__))
+             
+    return obj
+
+
+
+
+def getREF_class_name( REF_name):
+  if ref_dic.has_key(REF_name):
+    return ref_dic[str(REF_name)]
+  else:
+    return None
+  
+
+def getREF_var_name( REF_name):
+  if ref_class_dic.has_key(REF_name):
+    return ref_class_dic[str(REF_name)]
+  else:
+    return None
+
+def get_REF_object(ref_class_name):
+  """
+  Gets the Ref object based on class_name
+  """
+  class_ref=getattr(sys.modules[ref_pkg_dic[ref_class_name]], ref_class_name)
+  LOG.debug( class_ref )
+  return class_ref  
+
+def get_unicode( v):
+  import unicodedata
+  if v:
+    v = unicodedata.normalize('NFKD', v).encode('ascii', 'ignore')
+    LOG.debug( v )
+  return v
+  
+def retain_self_helper(self=None, **kwargs):
+    #print" ================== retain_self_helper\n"
+    #print locals()
+    from ambari_client.model.base_model import  BaseModel 
+    BaseModel.__init__(self, **kwargs)
+       
+def get_unicode_kw(dic):
+  """
+  We use unicode strings as keys in kwargs.
+  """
+  res = { }
+  for k, v in dic.iteritems():
+    res[str(k)] = v
+  return res

+ 44 - 0
ambari-client/src/main/python/ambari_client/resources/clusters.py

@@ -0,0 +1,44 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+  
+from ambari_client.model import  cluster
+__docformat__ = "epytext"
+
+
+def get_cluster(resource_root, cluster_name):
+  """
+  Get a cluster by cluster_name
+  @param resource_root: The root Resource.
+  @param cluster_name: Cluster cluster_name
+  @return: ClusterModel object
+  """
+  return cluster.get_cluster(resource_root, cluster_name)
+
+
+
+def get_all_clusters(root_resource):
+  """
+  Get all clusters in Ambari.
+  @param root_resource: The root Resource object.
+  @return: A list of ClusterModel objects in ModelList.
+  """
+  return cluster.get_all_clusters(root_resource)

+ 45 - 0
ambari-client/src/main/python/ambari_client/resources/services.py

@@ -0,0 +1,45 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+
+from ambari_client.model import  service
+__docformat__ = "epytext"
+
+
+def get_service(resource_root, service_name, cluster_name="default"):
+  """
+  Get a service by service_name
+  @param resource_root: The root Resource .
+  @param service_name: Service service_name.
+  @param cluster_name: Cluster service_name.
+  @return: ServiceModel object.
+  """
+  return  service.get_service(resource_root, service_name, cluster_name)
+
+
+
+def get_all_services(resource_root, cluster_name="default"):
+  """
+  Get all services.
+  @param resource_root: The root Resource.
+  @param cluster_name: Cluster name.
+  @return: A list of ServiceModel objects in ModelList.
+  """
+  return service.get_all_services(resource_root, cluster_name)

+ 101 - 0
ambari-client/src/main/python/ambari_client/rest_resource.py

@@ -0,0 +1,101 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+'''
+
+
+
+'''
+try:
+  import json
+except ImportError:
+  import simplejson as json
+import logging
+import posixpath
+
+LOG = logging.getLogger(__name__)
+
+
+class RestResource(object):
+  """
+  RestResource wrapper.
+  """
+  def __init__(self, client, path=""):
+    """
+    @param client: A Client object.
+    @param path: The relative path of the resource.
+    """
+    self._client = client
+    self._path = path.strip('/')
+
+  @property
+  def base_url(self):
+    return self._client.base_url
+
+  def _join_uri(self, relpath):
+    if relpath is None:
+      return self._path
+    return self._path + posixpath.normpath('/' + relpath)
+  
+  def _set_headers(self, content_type=None):
+    if content_type:
+      return { 'Content-Type': content_type }
+    return None
+
+  def invoke(self, http_method, url_path=None, payload=None, headers=None):
+    """
+    Invoke an API http_method.
+    """
+    path = self._join_uri(url_path)
+    resp ,code , content  = self._client.invoke(http_method, path,payload=payload,
+                                headers=headers)
+
+    LOG.debug ("RESPONSE from the REST request >>>>>>> \n"+str(resp) )
+    LOG.debug ("\n===========================================================")
+    if not resp and code!=200:
+        raise Exception("Command '%s %s' failed" %(http_method, path))
+    try:
+        if code==200 and not resp:
+          return {}
+        json_dict = json.loads(resp)
+        return json_dict
+    except Exception, ex:
+        LOG.error('JSON decode error: %s' % (resp,))
+        raise ex
+
+
+
+  def get(self, path=None):
+    """
+    Invoke the GET method .
+    @param path: resource path
+    @return: A dictionary of the REST result.
+    """
+    return self.invoke("GET", path)
+
+
+  def put(self, path=None, payload=None, content_type=None):
+    """
+    Invoke the PUT method on a resource.
+    @param path: resource path
+    @param payload: Body of the request.
+    @param content_type: 
+    @return: A dictionary of the REST result.
+    """
+    return self.invoke("PUT", path, payload,self._set_headers(content_type))
+
+
+

+ 39 - 0
ambari-client/src/main/python/setup.py

@@ -0,0 +1,39 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+
+from setuptools import setup, find_packages
+
+from sys import version_info, platform
+
+if version_info[:2] > (2, 5):
+    install_requires = []
+else:
+    install_requires = ['simplejson >= 2.0.0']
+
+# Python 2.6 and below requires argparse
+if version_info[:2] < (2, 7):
+    install_requires += ['argparse']
+
+setup(
+  name = 'ambari_client',
+  author_email = "ambari-dev@incubator.apache.org",
+  version = "1.0.3-SNAPSHOT",
+  packages = ['ambari_client'],
+  install_requires = install_requires,
+  description = 'Ambari python REST API client',
+  license = 'Apache License 2.0'
+)

+ 34 - 0
ambari-client/src/packages/tarball/all.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1 http://maven.apache.org/xsd/assembly-1.1.1.xsd">
+  <!--This 'all' id is not appended to the produced bundle because we do this:
+    http://maven.apache.org/plugins/maven-assembly-plugin/faq.html#required-classifiers
+  -->
+  <formats>
+    <format>dir</format>
+  </formats>
+  <includeBaseDirectory>false</includeBaseDirectory>
+  <fileSets>
+    <fileSet>
+      <directory>src/main/python</directory>
+      <outputDirectory>/</outputDirectory>
+    </fileSet>
+  </fileSets>
+</assembly>

+ 114 - 0
ambari-client/src/test/python/unitTests.py

@@ -0,0 +1,114 @@
+#!/usr/bin/env python2.6
+
+'''
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+'''
+
+import unittest
+import doctest
+from os.path import dirname, split, isdir
+import logging.handlers
+import logging
+
+LOG_FILE_NAME='tests.log'
+SELECTED_PREFIX = "_"
+PY_EXT='.py'
+
+class TestAgent(unittest.TestSuite):
+  def run(self, result):
+    run = unittest.TestSuite.run
+    run(self, result)
+    return result
+
+
+def parent_dir(path):
+  if isdir(path):
+    if path.endswith(os.sep):
+      path = os.path.dirname(path)
+    parent_dir = os.path.dirname(path)
+  else:
+    parent_dir = os.path.dirname(os.path.dirname(path))
+
+  return parent_dir
+
+
+def all_tests_suite():
+
+
+  src_dir = os.getcwd()
+  files_list=os.listdir(src_dir)
+  tests_list = []
+
+  logger.info('------------------------TESTS LIST:-------------------------------------')
+  # If test with special name exists, run only this test
+  selected_test = None
+  for file_name in files_list:
+    if file_name.endswith(PY_EXT) and not file_name == __file__ and file_name.startswith(SELECTED_PREFIX):
+      logger.info("Running only selected test " + str(file_name))
+      selected_test = file_name
+  if selected_test is not None:
+      tests_list.append(selected_test.replace(PY_EXT, ''))
+  else:
+    for file_name in files_list:
+      if file_name.endswith(PY_EXT) and not file_name == __file__:
+        logger.info(file_name)
+        tests_list.append(file_name.replace(PY_EXT, ''))
+  logger.info('------------------------------------------------------------------------')
+
+  suite = unittest.TestLoader().loadTestsFromNames(tests_list)
+  return TestAgent([suite])
+
+def main():
+
+  logger.info('------------------------------------------------------------------------')
+  logger.info('PYTHON AGENT TESTS')
+  logger.info('------------------------------------------------------------------------')
+  runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
+  suite = all_tests_suite()
+  status = runner.run(suite).wasSuccessful()
+
+  if not status:
+    logger.error('-----------------------------------------------------------------------')
+    logger.error('Python unit tests failed')
+    logger.error('Find detailed logs in ' + path)
+    logger.error('-----------------------------------------------------------------------')
+    exit(1)
+  else:
+    logger.info('------------------------------------------------------------------------')
+    logger.info('Python unit tests finished succesfully')
+    logger.info('------------------------------------------------------------------------')
+
+if __name__ == '__main__':
+  import os
+  import sys
+  import io
+  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + os.sep + 'main' + os.sep + 'python')
+  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + os.sep + 'main' + os.sep + 'python' + os.sep + 'ambari_agent')
+  logger = logging.getLogger()
+  logger.setLevel(logging.INFO)
+  formatter = logging.Formatter("[%(levelname)s] %(message)s")
+  src_dir = os.getcwd()
+  target_dir = parent_dir(parent_dir(parent_dir(src_dir))) + os.sep + 'target'
+  if not os.path.exists(target_dir):
+    os.mkdir(target_dir)
+  path = target_dir + os.sep + LOG_FILE_NAME
+  file=open(path, "w")
+  consoleLog = logging.StreamHandler(file)
+  consoleLog.setFormatter(formatter)
+  logger.addHandler(consoleLog)
+  main()