{"id":8473,"date":"2018-03-05T07:39:56","date_gmt":"2018-03-05T12:39:56","guid":{"rendered":"https:\/\/qxf2.com\/blog\/?p=8473"},"modified":"2020-01-30T11:58:06","modified_gmt":"2020-01-30T16:58:06","slug":"ssh-using-python-paramiko","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/ssh-using-python-paramiko\/","title":{"rendered":"SSH using Python Paramiko"},"content":{"rendered":"<p>Recently, as part of an automated test, we needed to <a href=\"https:\/\/www.ssh.com\/ssh\/\">SSH<\/a> into a server, toggle a service and then check the response on a web application. We used the Python module <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/\">Paramiko<\/a>. We ended up writing simple wrappers around the most common actions test automation may need to perform over SSH. In this post, we will share the same. <\/p>\n<p>We have written this tutorial for absolute beginners. If you are already familiar with SSH (there are people who are not!) and comfortable with Python, please jump ahead to the section &#8216;Putting it all together&#8217;.<\/p>\n<p><strong>Note 1:<\/strong> We have also integrated this code into our open-sourced <strong><a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\">Python test automation framework<\/a><\/strong>.<br \/>\n<strong>Note 2:<\/strong> The Python 3 code for this article is <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/utils\/ssh_util.py\">here<\/a><\/strong><\/p>\n<hr \/>\n<h3>How SSH Works<\/h3>\n<p>The SSH connection is implemented using a client-server model. To establish a connection the server should be running and clients generally authenticated either using passwords or SSH keys. Password Authentication is simple and straightforward. To authenticate using SSH keys, a user must have an SSH key pair (Public and Private key). On the remote server, the list of public keys is maintained (usually) in the ~\/.ssh\/authorized_keys directory. When the client connects to the remote server using the public key, the server checks for it and sends an encrypted message which can only be decrypted with the associated private key at the client side. We will be using a Python module called <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/\">Paramiko<\/a>. The Paramiko module gives an abstraction of the SSHv2 protocol with both the client side and server side functionality. As a client, you can authenticate yourself using a password or key and as a server, you can decide which users are allowed access and the channels you allow.<\/p>\n<hr \/>\n<h3>Installing Paramiko<\/h3>\n<p>Installing Paramiko is straightforward, it has only one direct hard dependency: the <a href=\"http:\/\/www.paramiko.org\/installing.html#cryptography\">Cryptography<\/a> library. If pip is above 8.x, nothing else is required. pip will install statically compiled binary archives of Cryptography its dependencies. For more details on installation, please refer to this <a href=\"http:\/\/www.paramiko.org\/installing.html\">Paramiko\u2019s documentation<\/a>.<\/p>\n<p>To install Paramiko on Windows<\/p>\n<pre lang=\"python\">pip install paramiko\r\n<\/pre>\n<hr \/>\n<h3>Overview of the Parmiko tutorial<\/h3>\n<p>In the coming sections, we will be talking about below items:<\/p>\n<li>Connecting to the remote server using Paramiko<\/li>\n<li>Executing commands on the server<\/li>\n<li>File transfers<\/li>\n<li>Putting it all together<\/li>\n<p>We have written different methods for each functionality. All the parameters are read from a configuration file. We are reading the parameters like credentials, commands &#038; file paths from the conf file because when there is any change in the machine or any credential changes, changing the configuration file alone is enough instead of changing the script.<\/p>\n<p>After importing Paramiko, to make everything more modular, we created a class called Ssh_Util and wrote different methods for each functionality.<\/p>\n<hr \/>\n<h3>Connecting to the remote server using Paramiko<\/h3>\n<p>Firstly, we created an initialization function inside the class and initialized required variables. Some of the parameters like host, username, password etc., are read from a configuration file.<\/p>\n<pre lang=\"python\">\r\ndef __init__(self):\r\n        self.ssh_output = None\r\n        self.ssh_error = None\r\n        self.client = None\r\n        self.host= conf_file.HOST\r\n        self.username = conf_file.USERNAME\r\n        self.password = conf_file.PASSWORD\r\n        self.timeout = float(conf_file.TIMEOUT)\r\n        self.commands = conf_file.COMMANDS\r\n        self.pkey = conf_file.PKEY\r\n        self.port = conf_file.PORT\r\n        self.uploadremotefilepath = conf_file.UPLOADREMOTEFILEPATH\r\n        self.uploadlocalfilepath = conf_file.UPLOADLOCALFILEPATH\r\n        self.downloadremotefilepath = conf_file.DOWNLOADREMOTEFILEPATH\r\n        self.downloadlocalfilepath = conf_file.DOWNLOADLOCALFILEPATH\r\n<\/pre>\n<p>We created a function connect() and placed inside the Ssh_Util class. This function will be used to connect to the remote server.<\/p>\n<pre lang=\"python\">\r\ndef connect(self):\r\n        \"Login to the remote server\"\r\n        try:\r\n            #Paramiko.SSHClient can be used to make connections to the remote server and transfer files\r\n            print(\"Establishing ssh connection\")\r\n            self.client = paramiko.SSHClient()\r\n            #Parsing an instance of the AutoAddPolicy to set_missing_host_key_policy() changes it to allow any host.\r\n            self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())\r\n            #Connect to the server\r\n            if (self.password == ''):\r\n                private_key = paramiko.RSAKey.from_private_key_file(self.pkey)\r\n                self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=private_key, timeout=self.timeout, allow_agent=False,look_for_keys=False)\r\n                print(\"Connected to the server\",self.host)\r\n            else:\r\n                self.client.connect(hostname=self.host, port=self.port, username=self.username, password=self.password,timeout=self.timeout, allow_agent=False,look_for_keys=False)    \r\n                print(\"Connected to the server\",self.host)\r\n        except paramiko.AuthenticationException:\r\n            print(\"Authentication failed, please verify your credentials\")\r\n            result_flag = False\r\n        except paramiko.SSHException as sshException:\r\n            print(\"Could not establish SSH connection: %s\" % sshException)\r\n            result_flag = False\r\n        except socket.timeout as e:\r\n            print(\"Connection timed out\")\r\n            result_flag = False\r\n        except Exception,e:\r\n            print(\"Exception in connecting to the server\")\r\n            print(\"PYTHON SAYS:\",e)\r\n            result_flag = False\r\n            self.client.close()\r\n        else:\r\n            result_flag = True\r\n        \r\n        return result_flag \r\n<\/pre>\n<p>Firstly, we created an instance &#8216;client&#8217; of <em>paramiko.SSHClient()<\/em>. <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html\">Paramiko.SSHClient<\/a> is the primary client used to make connections to the remote server and execute commands. This class wraps <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/transport.html\">Transport<\/a>, <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/channel.html\">Channel<\/a>, and <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html\">SFTPClient<\/a> to take care of most aspects of authenticating and opening channels. The creation of an SSHClient object allows establishing server connections via the connect() method.<\/p>\n<p>When we try to connect to a server for the first time, we see an error message prompted saying that the machine is not informed about the remote server that we are trying to access. Paramiko&#8217;s <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.SSHClient.set_missing_host_key_policy\">client.set_missing_host_key_policy<\/a> is the method used for this purpose. By default, the paramiko.SSHclient sets the policy to the <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.RejectPolicy\">Reject<\/a> policy which means the policy rejects connection without validating. In our code, we are overriding this by passing <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.AutoAddPolicy\">AutoAddPolicy()<\/a> function where the new server will automatically add the server&#8217;s host key without prompting. <strong>Note:-<\/strong> In a testing environment, we can use <em>set_missing_host_key_policy<\/em> and set <em>AutoAddPolicy<\/em> but for security purpose, this is not a good idea to use in production.<\/p>\n<p>Paramiko supports both password-based authentication and key-pair based authentication for accessing the server. In our code, we are checking for a password and if available, authentication is attempted using plain username\/password authentication. If the password is not available, authentication is attempted by reading the private key file.<\/p>\n<p><a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.SSHClient.connect\">client.Connect()<\/a> is to connect to an SSH server. This method also allows to provide our own private key, or connect to the SSH agent on the local machine or read from the user&#8217;s local key files. We also used timeout (in seconds) to wait for an authentication response. We are capturing a couple of exceptions like paramiko.AuthenticationException, paramiko.SSHException, Socket.timeout to handle the errors while connecting.<\/p>\n<hr \/>\n<h3>Executing commands on the server<\/h3>\n<p>Now, you are connected to the remote server. The next step is to execute commands on the SSH server. To run a command on the server the <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.SSHClient.exec_command\">exec_command()<\/a> function is called on the SSHClient with the command passed as input. When you execute commands using exec_command a new Channel is opened and the requested command is executed. The response is returned as Python file-like objects representing stdin, stdout, and stderr(as a 3-tuple)<\/p>\n<ul>\n<li>The <em>stdin <\/em>is a write-only file which can be used for input commands.<\/li>\n<li>The <em>stdout<\/em> file give the output of the command.<\/li>\n<li>The <em>stderr<\/em> gives the errors returned on executing the command. Will be empty if there is no error.<\/li>\n<\/ul>\n<p>We have written below function execute_command() to execute the commands. The input for this function is the set of commands. The call to the function connect() is made first and once the connection is established it is followed by a call to function exec_command(). We are storing the stdout and stderr into two variables namely ssh_output and ssh_error respectively. The ssh_output is the output to standard output as produced when the command executed on the remote computer. The ssh_error is the output of standard error produced at the same occasion. The user can use these variables for further processing if needed.<\/p>\n<pre lang=\"python\">\r\ndef execute_command(self,commands):\r\n        \"\"\"Execute a command on the remote host.Return a tuple containing\r\n        an integer status and a two strings, the first containing stdout\r\n        and the second containing stderr from the command.\"\"\"\r\n        self.ssh_output = None\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                for command in commands:\r\n                    print(\"Executing command --> {}\".format(command))\r\n                    stdin, stdout, stderr = self.client.exec_command(command,timeout=10)\r\n                    self.ssh_output = stdout.read()\r\n                    self.ssh_error = stderr.read()\r\n                    if self.ssh_error:\r\n                        print(\"Problem occurred while running command:\"+ command + \" The error is \" + self.ssh_error)\r\n                        result_flag = False\r\n                    else:    \r\n                        print(\"Command execution completed successfully\",command)\r\n                    self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False   \r\n        except socket.timeout as e:\r\n            print(\"Command timed out.\", command)\r\n            self.client.close()\r\n            result_flag = False                \r\n        except paramiko.SSHException:\r\n            print(\"Failed to execute the command!\",command)\r\n            self.client.close()\r\n            result_flag = False    \r\n                      \r\n        return result_flag\r\n<\/pre>\n<hr \/>\n<h3>File Transfers<\/h3>\n<p>File transfers are needed to perform remote file operations. Paramiko allows to programmatically send and receive files using the <a href=\"http:\/\/href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/sftp.html\">SFTP protocol<\/a>, the connection with the remote host is established in the same way explained in the previous sections, the call to connect() is followed by a call to <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/client.html#paramiko.client.SSHClient.open_sftp\">open_sftp()<\/a> that returns a new SFTPClient session object. This object allows to perform common SFTP operations like <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/sftp.html#paramiko.sftp_client.SFTPClient.get\">get()<\/a>, <a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/sftp.html#paramiko.sftp_client.SFTPClient.put\">put()<\/a>,<a href=\"http:\/\/docs.paramiko.org\/en\/2.4\/api\/sftp.html#paramiko.sftp_client.SFTPClient.listdir\">listdir()<\/a>.<\/p>\n<p>We have created two functions(upload_file() and download_file()) to upload a file to the remote server and download a file from the remote server.<\/p>\n<p>Firstly, we created a function <em>upload_file()<\/em> which uploads a file to the remote server. The code below establishes the SFTP Connection using the SSH client and uploads a file. The <em>put()<\/em> method will copy a local file (local path) to the SFTP server as the remote path. <strong>Note:-<\/strong> The filename should be included. Only specifying a directory may result in an error. Once the operation is done you may close the SFTP session and its underlying channel using ftp_client.close().<\/p>\n<pre lang=\"python\">\r\ndef upload_file(self,uploadlocalfilepath,uploadremotefilepath):\r\n        \"This method uploads the file to remote server\"\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                ftp_client= self.client.open_sftp()\r\n                ftp_client.put(uploadlocalfilepath,uploadremotefilepath)\r\n                ftp_client.close() \r\n                self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False  \r\n        except Exception,e:\r\n            print('\\nUnable to upload the file to the remote server',uploadremotefilepath)\r\n            print('PYTHON SAYS:',e)\r\n            result_flag = False\r\n            ftp_client.close()\r\n            self.client.close()\r\n        \r\n        return result_flag\r\n<\/pre>\n<p>Similarly, <em>download_file()<\/em> function downloads a file from the remote server. The code below establishes the SFTP Connection using the SSH client and downloads a file. The <em>get()<\/em> method will copy a remote file (remote path) from the SFTP server to the local host as local path. Once the operation is done you may close the SFTP session and its underlying channel using ftp_client.close().<\/p>\n<pre lang=\"python\">\r\ndef download_file(self,downloadremotefilepath,downloadlocalfilepath):\r\n        \"This method downloads the file from remote server\"\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                ftp_client= self.client.open_sftp()\r\n                ftp_client.get(downloadremotefilepath,downloadlocalfilepath)\r\n                ftp_client.close()  \r\n                self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False  \r\n        except Exception,e:\r\n            print('\\nUnable to download the file from the remote server',downloadremotefilepath)\r\n            print('PYTHON SAYS:',e)\r\n            result_flag = False\r\n            ftp_client.close()\r\n            self.client.close()\r\n        \r\n        return result_flag\r\n<\/pre>\n<hr \/>\n<h3>Putting it all together<\/h3>\n<p><strong>Note:<\/strong> The Python 3 code for this article is <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/utils\/ssh_util.py\">here<\/a><\/strong>. Please use it instead of the file below.<\/p>\n<pre lang=\"python\">\r\nimport paramiko\r\nimport os,sys,time\r\nsys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\r\nfrom conf import ssh_conf as conf_file   \r\nimport socket\r\n\r\nclass Ssh_Util:\r\n    \"Class to connect to remote server\" \r\n    \r\n    def __init__(self):\r\n        self.ssh_output = None\r\n        self.ssh_error = None\r\n        self.client = None\r\n        self.host= conf_file.HOST\r\n        self.username = conf_file.USERNAME\r\n        self.password = conf_file.PASSWORD\r\n        self.timeout = float(conf_file.TIMEOUT)\r\n        self.commands = conf_file.COMMANDS\r\n        self.pkey = conf_file.PKEY\r\n        self.port = conf_file.PORT\r\n        self.uploadremotefilepath = conf_file.UPLOADREMOTEFILEPATH\r\n        self.uploadlocalfilepath = conf_file.UPLOADLOCALFILEPATH\r\n        self.downloadremotefilepath = conf_file.DOWNLOADREMOTEFILEPATH\r\n        self.downloadlocalfilepath = conf_file.DOWNLOADLOCALFILEPATH\r\n       \r\n    def connect(self):\r\n        \"Login to the remote server\"\r\n        try:\r\n            #Paramiko.SSHClient can be used to make connections to the remote server and transfer files\r\n            print(\"Establishing ssh connection\")\r\n            self.client = paramiko.SSHClient()\r\n            #Parsing an instance of the AutoAddPolicy to set_missing_host_key_policy() changes it to allow any host.\r\n            self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())\r\n            #Connect to the server\r\n            if (self.password == ''):\r\n                self.pkey = paramiko.RSAKey.from_private_key_file(self.pkey)\r\n                self.client.connect(hostname=self.host, port=self.port, username=self.username,pkey=self.pkey ,timeout=self.timeout, allow_agent=False, look_for_keys=False)\r\n                print(\"Connected to the server\",self.host)\r\n            else:\r\n                self.client.connect(hostname=self.host, port=self.port,username=self.username,password=self.password,timeout=self.timeout, allow_agent=False, look_for_keys=False)    \r\n                print(\"Connected to the server\",self.host)\r\n        except paramiko.AuthenticationException:\r\n            print(\"Authentication failed, please verify your credentials\")\r\n            result_flag = False\r\n        except paramiko.SSHException as sshException:\r\n            print(\"Could not establish SSH connection: %s\" % sshException)\r\n            result_flag = False\r\n        except socket.timeout as e:\r\n            print(\"Connection timed out\")\r\n            result_flag = False\r\n        except Exception,e:\r\n            print('\\nException in connecting to the server')\r\n            print('PYTHON SAYS:',e)\r\n            result_flag = False\r\n            self.client.close()\r\n        else:\r\n            result_flag = True\r\n        \r\n        return result_flag    \r\n  \r\n    def execute_command(self,commands):\r\n        \"\"\"Execute a command on the remote host.Return a tuple containing\r\n        an integer status and a two strings, the first containing stdout\r\n        and the second containing stderr from the command.\"\"\"\r\n        self.ssh_output = None\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                for command in commands:\r\n                    print(\"Executing command --> {}\".format(command))\r\n                    stdin, stdout, stderr = self.client.exec_command(command,timeout=10)\r\n                    self.ssh_output = stdout.read()\r\n                    self.ssh_error = stderr.read()\r\n                    if self.ssh_error:\r\n                        print(\"Problem occurred while running command:\"+ command + \" The error is \" + self.ssh_error)\r\n                        result_flag = False\r\n                    else:    \r\n                        print(\"Command execution completed successfully\",command)\r\n                    self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False   \r\n        except socket.timeout as e:\r\n            print(\"Command timed out.\", command)\r\n            self.client.close()\r\n            result_flag = False                \r\n        except paramiko.SSHException:\r\n            print(\"Failed to execute the command!\",command)\r\n            self.client.close()\r\n            result_flag = False    \r\n                      \r\n        return result_flag\r\n\r\n    def upload_file(self,uploadlocalfilepath,uploadremotefilepath):\r\n        \"This method uploads the file to remote server\"\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                ftp_client= self.client.open_sftp()\r\n                ftp_client.put(uploadlocalfilepath,uploadremotefilepath)\r\n                ftp_client.close() \r\n                self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False  \r\n        except Exception,e:\r\n            print('\\nUnable to upload the file to the remote server',uploadremotefilepath)\r\n            print('PYTHON SAYS:',e)\r\n            result_flag = False\r\n            ftp_client.close()\r\n            self.client.close()\r\n        \r\n        return result_flag\r\n\r\n    def download_file(self,downloadremotefilepath,downloadlocalfilepath):\r\n        \"This method downloads the file from remote server\"\r\n        result_flag = True\r\n        try:\r\n            if self.connect():\r\n                ftp_client= self.client.open_sftp()\r\n                ftp_client.get(downloadremotefilepath,downloadlocalfilepath)\r\n                ftp_client.close()  \r\n                self.client.close()\r\n            else:\r\n                print(\"Could not establish SSH connection\")\r\n                result_flag = False  \r\n        except Exception,e:\r\n            print('\\nUnable to download the file from the remote server',downloadremotefilepath)\r\n            print('PYTHON SAYS:',e)\r\n            result_flag = False\r\n            ftp_client.close()\r\n            self.client.close()\r\n        \r\n        return result_flag\r\n\r\n\r\n#---USAGE EXAMPLES\r\nif __name__=='__main__':\r\n    print(\"Start of %s\"%__file__)\r\n     \r\n    #Initialize the ssh object\r\n    ssh_obj = Ssh_Util()\r\n\r\n    #Sample code to execute commands\r\n    if ssh_obj.execute_command(ssh_obj.commands) is True:\r\n        print(\"Commands executed successfully\\n\")\r\n    else:\r\n        print(\"Unable to execute the commands\")\r\n    \r\n    \"\"\"\r\n    #Sample code to upload a file to the server\r\n    if ssh_obj.upload_file(ssh_obj.uploadlocalfilepath,ssh_obj.uploadremotefilepath) is True:\r\n        print(\"File uploaded successfully\", ssh_obj.uploadremotefilepath)\r\n    else:\r\n        print(\"Failed to upload the file\")\r\n    \r\n    #Sample code to download a file from the server\r\n    if ssh_obj.download_file(ssh_obj.downloadremotefilepath,ssh_obj.downloadlocalfilepath) is True:\r\n        print(\"File downloaded successfully\", ssh_obj.downloadlocalfilepath)\r\n    else:\r\n        print(\"Failed to download the file\")\r\n    \"\"\"\r\n<\/pre>\n<hr \/>\n<p>Hope this code helps you get started with Paramiko easily. Happy coding! If you liked this article and want to learn more about Qxf2 and our testing services for startups, click <a href=\"https:\/\/qxf2.com\/blog\/about-qxf2\/\">here<\/a>.<\/p>\n<hr \/>\n<h3>References<\/h3>\n<p>1)<a href=\"https:\/\/medium.com\/@keagileageek\/paramiko-how-to-ssh-and-file-transfers-with-python-75766179de73\"> SSH and transfer file using paramiko<\/a><br \/>\n2)<a href=\"https:\/\/www.minvolai.com\/how-to-ssh-in-python-using-paramiko\/\"> How to SSH in Python using paramiko<\/a><br \/>\n3)<a href=\"https:\/\/github.com\/paramiko\/paramiko\"> Paramiko Github examples<\/a><br \/>\n4)<a href=\"https:\/\/www.hostinger.com\/tutorials\/ssh-tutorial-how-does-ssh-work\"> How does SSH works?<\/a><\/p>\n<hr>\n","protected":false},"excerpt":{"rendered":"<p>Recently, as part of an automated test, we needed to SSH into a server, toggle a service and then check the response on a web application. We used the Python module Paramiko. We ended up writing simple wrappers around the most common actions test automation may need to perform over SSH. In this post, we will share the same. We [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38,157,18,158],"tags":[],"class_list":["post-8473","post","type-post","status-publish","format-standard","hentry","category-automation","category-paramiko","category-python","category-ssh"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/8473","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=8473"}],"version-history":[{"count":128,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/8473\/revisions"}],"predecessor-version":[{"id":12294,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/8473\/revisions\/12294"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=8473"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=8473"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=8473"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}